diff --git a/luci-app-adguardhome/.official_channel b/luci-app-adguardhome/.official_channel index 8ab67f2e7..49f33a8c6 100644 --- a/luci-app-adguardhome/.official_channel +++ b/luci-app-adguardhome/.official_channel @@ -1 +1 @@ -snapshot +both diff --git a/luci-app-adguardhome/.official_version_release b/luci-app-adguardhome/.official_version_release new file mode 100644 index 000000000..a9b889cb7 --- /dev/null +++ b/luci-app-adguardhome/.official_version_release @@ -0,0 +1 @@ +26.273.20498~1fcad1e diff --git a/luci-app-adguardhome/.official_json_version b/luci-app-adguardhome/.official_version_snapshot similarity index 100% rename from luci-app-adguardhome/.official_json_version rename to luci-app-adguardhome/.official_version_snapshot diff --git a/luci-theme-glass/.gitignore b/luci-theme-glass/.gitignore new file mode 100644 index 000000000..d90e76d4e --- /dev/null +++ b/luci-theme-glass/.gitignore @@ -0,0 +1,24 @@ +# Internal use +docs/ +/resources/ + +# Hidden dotfiles/folders +.* +!.gitignore +!.gitkeep + +# Editor files +*.swp +*.swo +*~ + +# Signing keys (secret key is in GitHub Secrets, NEVER commit) +/keys/ + +# Build output +dist/ + +# Node modules (for LESS compilation) +node_modules/ +package-lock.json + diff --git a/luci-theme-glass/LICENSE b/luci-theme-glass/LICENSE new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/luci-theme-glass/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/luci-theme-glass/Makefile b/luci-theme-glass/Makefile new file mode 100644 index 000000000..7cde38b34 --- /dev/null +++ b/luci-theme-glass/Makefile @@ -0,0 +1,19 @@ +# +# Copyright (C) 2024-2026 luci-theme-glass contributors +# +# This is free software, licensed under the GNU General Public License v3.0. +# + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=Glass - Apple-inspired glassmorphism theme for LuCI +LUCI_DEPENDS:= +PKG_VERSION:=$(shell cat $(CURDIR)/ucode/template/themes/glass/version | tr -d '[:space:]') +PKG_RELEASE:=1 + +# Disable CSS minification (backdrop-filter can break with csstidy) +CONFIG_LUCI_CSSTIDY:= + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-theme-glass/README.md b/luci-theme-glass/README.md new file mode 100644 index 000000000..64bf7bb45 --- /dev/null +++ b/luci-theme-glass/README.md @@ -0,0 +1,236 @@ +# luci-theme-glass + +A glassmorphism theme for OpenWrt LuCI, inspired by Apple's visionOS and macOS. Frosted-glass panels, backdrop blur, translucent buttons, and inset highlights across the entire UI. + +**[Live Demo](https://rchen14b.github.io/luci-theme-glass/)** | **[Download](https://github.com/rchen14b/luci-theme-glass/releases/latest)** + +![License](https://img.shields.io/badge/license-GPL--3.0%20%2F%20Commercial-blue.svg) +![OpenWrt](https://img.shields.io/badge/OpenWrt-23.05%2B-brightgreen.svg) +![Version](https://img.shields.io/badge/version-1.1.6-orange.svg) +[![Buy Me A Coffee](https://img.shields.io/badge/Buy%20Me%20A%20Coffee-support-yellow?style=flat&logo=buy-me-a-coffee)](https://buymeacoffee.com/rchen14b) + +## Screenshots + +### Login +![Login](screenshots/Login_page_light.png) + +### Overview (Light) +![Overview Light](screenshots/Overview_page_light.png) + +### Overview (Dark) +![Overview Dark](screenshots/Overview_page_dark.png) + +### Software +![Software](screenshots/Software_page_light.png) + +### Traffic Graph +![Traffic](screenshots/Traffic_graph_light.png) + +## Features + +### Design +- **Glassmorphism everywhere** — `backdrop-filter: blur() saturate()` with multi-stop gradients, translucent backgrounds, 0.5px glass borders, and inset glow highlights on all panels +- **Glass buttons** — Neutral buttons are translucent glass, primary buttons are blue-tinted, danger buttons are red-tinted. No solid-color buttons +- **Frosted sidebar** — Fixed sidebar with 30px blur and inset light diffusion +- **Glass header** — Top nav bar with glass pill badges for page title and status indicators +- **Live status indicators** — CPU load, RAM %, uptime, and WAN throughput in the header bar. Auto-detects WAN interface, resolves bridges, handles DSA hardware offloading. MikroTik-style speed formatting. Color-coded levels and detailed tooltips +- **Glass login card** — Frosted login page with translucent inputs + +### Theming +- **Auto / Light / Dark mode** — Toggle in the header bar cycles through Auto (follows OS `prefers-color-scheme`), Light, and Dark. Works on every page including login. Saved to `localStorage` so it persists across sessions +- **Dark mode glass** — Lower-opacity gradients (`rgba(255,255,255,0.05-0.08)`) with adjusted shadows +- **Server-side default** — Set default mode via UCI config (`option mode 'dark'`, `'light'`, or `'normal'` for auto). The client-side toggle overrides this per browser +- **Accent color config** — Primary color, blur radius, and glass transparency are adjustable via UCI +- **Custom wallpapers** — Drop an image or video into the background folder. It shows through all glass panels + +### Layout +- **Responsive** — Sidebar collapses to a slide-out drawer on mobile with hamburger toggle +- **Sub-navigation bar** — CBI tab menus get moved into a secondary glass bar below the header +- **Sticky header** — Fixed header with smooth transitions + +### Compatibility +- **Full LuCI coverage** — Styles all CBI components: sections, tables, forms, dropdowns, checkboxes, textareas, progress bars, tooltips, modals, tab menus +- **Page-specific fixes** — Overrides inline CSS from LuCI view JS (package manager layout, port status grid, network status cards) +- **OpenWrt 23.05+** — Uses ucode `.ut` templates +- **No dependencies** — Pure CSS, no JS frameworks + +## Installation + +### From package repository (auto-update, signed) + +Add the repository to get updates via the package manager. Both opkg and apk feeds are signed. + +**OpenWrt 24.10 and earlier (opkg):** +```sh +# First install: fetch signing public key (one-time) +wget -O /etc/opkg/keys/ef97c468af77cea1 \ + https://raw.githubusercontent.com/rchen14b/luci-theme-glass/main/root/etc/opkg/keys/ef97c468af77cea1 + +# Add feed and install +echo "src/gz glass https://rchen14b.github.io/luci-theme-glass/packages" >> /etc/opkg/customfeeds.conf +opkg update +opkg install luci-theme-glass +``` + +**OpenWrt 25.12+ (apk):** +```sh +# First install: fetch signing public key (one-time) +wget -O /etc/apk/keys/glass-apk.rsa.pub \ + https://raw.githubusercontent.com/rchen14b/luci-theme-glass/main/root/etc/apk/keys/glass-apk.rsa.pub + +# Add feed and install +echo "https://rchen14b.github.io/luci-theme-glass/apk/packages.adb" >> /etc/apk/repositories.d/customfeeds.list +apk update +apk add luci-theme-glass +``` + +After the first install, the public key persists on the router — future updates via `opkg upgrade` / `apk upgrade` just work. The LuCI web-gui "Update lists" button also works without warnings. + +### From release (manual) + +Download the latest package from [Releases](../../releases): + +- **OpenWrt 25.12+** (apk): `apk add --allow-untrusted luci-theme-glass-*.apk` +- **OpenWrt 24.10 and earlier** (opkg): `opkg install luci-theme-glass_*.ipk` + +### From source + +```sh +# Add to your OpenWrt build tree +cd /path/to/openwrt +git clone https://github.com/rchen14b/luci-theme-glass.git package/luci-theme-glass + +# Build +make package/luci-theme-glass/compile V=s +``` + +### Manual install (dev) + +```sh +# Copy theme files to router +scp -r htdocs/luci-static/glass/ root@router:/www/luci-static/glass/ +scp htdocs/luci-static/resources/menu-glass.js root@router:/www/luci-static/resources/ +scp -r ucode/template/themes/glass/ root@router:/usr/share/ucode/luci/template/themes/glass/ + +# Register and activate +ssh root@router "uci set luci.themes.Glass=/luci-static/glass && \ + uci set luci.main.mediaurlbase=/luci-static/glass && \ + uci commit luci" +``` + +## Configuration + +After installation, select **Glass** in **System > System > Language and Style**. + +### Theme config + +Create `/etc/config/glass` on the router: + +``` +config global + option mode 'normal' + option primary '#007AFF' + option dark_primary '#0A84FF' + option blur '20' + option transparency '0.72' + option blur_dark '25' + option transparency_dark '0.30' +``` + +| Option | Default | Description | +|--------|---------|-------------| +| `mode` | `normal` | `normal` (auto), `dark`, or `light` | +| `primary` | `#007AFF` | Accent color (light mode) | +| `dark_primary` | `#0A84FF` | Accent color (dark mode) | +| `blur` | `20` | Backdrop blur in px (light mode) | +| `transparency` | `0.72` | Glass panel opacity 0-1 (light mode) | +| `blur_dark` | `25` | Backdrop blur in px (dark mode) | +| `transparency_dark` | `0.30` | Glass panel opacity 0-1 (dark mode) | + +### Custom wallpapers + +Place a file named `bg.*` in `/www/luci-static/glass/background/` on the router. + +Supported: `.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`, `.mp4`, `.webm` + +The background shows through all glass panels. + +## Development + +### Prerequisites + +- Node.js with `lessc`: `npm install -g less` + +### Building CSS + +```sh +lessc less/cascade.less htdocs/luci-static/glass/css/cascade.css +lessc less/dark.less htdocs/luci-static/glass/css/dark.css +``` + +### Project structure + +``` +luci-theme-glass/ +├── Makefile # OpenWrt package build +├── htdocs/luci-static/ +│ ├── glass/ +│ │ ├── css/ # Compiled CSS +│ │ ├── img/ # Logo and icons +│ │ └── background/ # User wallpapers (bg.jpg, etc.) +│ └── resources/ +│ ├── menu-glass.js # Client-side menu renderer +│ └── status-glass.js # Header status indicators (CPU, RAM, net, uptime) +├── less/ # LESS source +│ ├── cascade.less # Master import file +│ ├── variables.less # Design tokens (CSS custom properties) +│ ├── normalize.less # CSS reset +│ ├── glass.less # Glassmorphism mixins +│ ├── layout.less # Sidebar, header, footer, content +│ ├── components.less # CBI forms, tables, buttons, alerts +│ ├── sysauth.less # Login page +│ ├── page-fix.less # LuCI page-specific overrides +│ ├── dark.less # Dark mode overrides +│ └── responsive.less # Mobile breakpoints +├── ucode/template/themes/glass/ # Server-side ucode templates +│ ├── header.ut # HTML head + sidebar + header +│ ├── header_login.ut # Login page variant +│ ├── footer.ut # Footer + scripts +│ ├── footer_login.ut # Login footer variant +│ └── sysauth.ut # Login page +├── root/ # Files installed to device root +│ ├── etc/uci-defaults/ # First-boot theme registration +│ └── usr/share/rpcd/acl.d/ # ACL permissions +└── screenshots/ +``` + +### Design system + +Design tokens live in `variables.less`. Glass effects use three LESS mixins: + +| Mixin | Use | Blur | Opacity | +|-------|-----|------|---------| +| `.glass-effect()` | Panels, cards | `var(--glass-blur)` | `var(--glass-bg)` | +| `.glass-frosted()` | Sidebar, login card | 30px | Higher | +| `.glass-subtle()` | Secondary elements | Lower | Minimal | + +### Version bump checklist + +1. `Makefile` — `PKG_VERSION` +2. `README.md` — version badge +3. `ucode/template/themes/glass/sysauth.ut` — login version text +4. `ucode/template/themes/glass/header.ut` — sidebar version text +5. `ucode/template/themes/glass/footer.ut` — footer version text + +## Credits + +- Architecture based on [luci-theme-argon](https://github.com/jerrykuku/luci-theme-argon) +- Glass effects from [glass-ui](https://github.com/crenspire/glass-ui) +- Design language from Apple visionOS / macOS + +## License + +Dual-licensed: + +- **Open Source**: [GPL-3.0](LICENSE) — free for personal, educational, and open-source use. Derivatives must also be GPL-3.0. +- **Commercial**: Proprietary/closed-source use requires a separate license. Contact the author. diff --git a/luci-theme-glass/build-pkg.sh b/luci-theme-glass/build-pkg.sh new file mode 100755 index 000000000..4fef455c4 --- /dev/null +++ b/luci-theme-glass/build-pkg.sh @@ -0,0 +1,151 @@ +#!/bin/bash +set -e + +PKG_NAME="luci-theme-glass" +PKG_VERSION="$(tr -d '[:space:]' < "$(dirname "$0")/ucode/template/themes/glass/version")" +PKG_RELEASE="1" +MAINTAINER="Ryan Chen " +DESCRIPTION="Glass - Apple-inspired glassmorphism theme for LuCI" +HOMEPAGE="https://github.com/rchen14b/luci-theme-glass" +LICENSE="GPL-3.0" + +DIST_DIR="dist" +WORK_DIR=$(mktemp -d) + +# Use GNU tar (gtar) on macOS, fall back to tar +TAR=$(command -v gtar 2>/dev/null || echo tar) + +trap "rm -rf $WORK_DIR" EXIT + +echo "==> Preparing file tree..." + +DATA_DIR="$WORK_DIR/data" +mkdir -p "$DATA_DIR/www/luci-static/glass" +mkdir -p "$DATA_DIR/www/luci-static/resources" +mkdir -p "$DATA_DIR/www/luci-static/resources/view/system" +mkdir -p "$DATA_DIR/usr/share/ucode/luci/template/themes/glass" +mkdir -p "$DATA_DIR/usr/share/rpcd/acl.d" +mkdir -p "$DATA_DIR/usr/share/luci/menu.d" +mkdir -p "$DATA_DIR/etc/uci-defaults" +mkdir -p "$DATA_DIR/etc/config" +mkdir -p "$DATA_DIR/etc/opkg/keys" +mkdir -p "$DATA_DIR/etc/apk/keys" + +cp -r htdocs/luci-static/glass/css "$DATA_DIR/www/luci-static/glass/" +cp -r htdocs/luci-static/glass/img "$DATA_DIR/www/luci-static/glass/" +mkdir -p "$DATA_DIR/www/luci-static/glass/background" +cp htdocs/luci-static/resources/menu-glass.js "$DATA_DIR/www/luci-static/resources/" +cp htdocs/luci-static/resources/status-glass.js "$DATA_DIR/www/luci-static/resources/" +cp htdocs/luci-static/resources/view/system/glass.js "$DATA_DIR/www/luci-static/resources/view/system/" +cp ucode/template/themes/glass/*.ut "$DATA_DIR/usr/share/ucode/luci/template/themes/glass/" +cp ucode/template/themes/glass/version "$DATA_DIR/usr/share/ucode/luci/template/themes/glass/" +cp root/usr/share/rpcd/acl.d/luci-theme-glass.json "$DATA_DIR/usr/share/rpcd/acl.d/" +cp root/usr/share/luci/menu.d/luci-theme-glass.json "$DATA_DIR/usr/share/luci/menu.d/" +cp root/etc/uci-defaults/30_luci-theme-glass "$DATA_DIR/etc/uci-defaults/" +cp root/etc/config/glass "$DATA_DIR/etc/config/" +cp root/etc/opkg/keys/* "$DATA_DIR/etc/opkg/keys/" 2>/dev/null || true +cp root/etc/apk/keys/* "$DATA_DIR/etc/apk/keys/" 2>/dev/null || true + +INSTALLED_SIZE=$(du -sk "$DATA_DIR" | cut -f1) +INSTALLED_BYTES=$((INSTALLED_SIZE * 1024)) +BUILD_DATE=$(date +%s) + +mkdir -p "$DIST_DIR" + +# ============================================================ +# Build IPK (opkg — OpenWrt 24.10 and earlier) +# ============================================================ +echo "==> Building IPK..." + +IPK_DIR="$WORK_DIR/ipk" +mkdir -p "$IPK_DIR/control" + +cat > "$IPK_DIR/control/control" < "$IPK_DIR/control/postinst" <<'SCRIPT' +#!/bin/sh +if [ "$PKG_UPGRADE" != 1 ]; then + uci get luci.themes.Glass >/dev/null 2>&1 || \ + uci batch <<-EOF + set luci.themes.Glass=/luci-static/glass + set luci.main.mediaurlbase=/luci-static/glass + commit luci + EOF +fi +exit 0 +SCRIPT +chmod 755 "$IPK_DIR/control/postinst" + +echo "2.0" > "$IPK_DIR/debian-binary" + +(cd "$DATA_DIR" && $TAR --format=gnu --numeric-owner --owner=0 --group=0 -cf - . | gzip -n > "$IPK_DIR/data.tar.gz") +(cd "$IPK_DIR/control" && $TAR --format=gnu --numeric-owner --owner=0 --group=0 -cf - . | gzip -n > "$IPK_DIR/control.tar.gz") +(cd "$IPK_DIR" && $TAR --format=gnu --numeric-owner --owner=0 --group=0 -cf - ./debian-binary ./data.tar.gz ./control.tar.gz | gzip -n > "$OLDPWD/$DIST_DIR/${PKG_NAME}_${PKG_VERSION}-${PKG_RELEASE}_all.ipk") + +echo " -> $DIST_DIR/${PKG_NAME}_${PKG_VERSION}-${PKG_RELEASE}_all.ipk" + +# ============================================================ +# Build APK (apk-tools v3 ADB format — OpenWrt 25.12+) +# Requires Docker (uses Alpine container with apk mkpkg) +# ============================================================ +echo "==> Building APK..." + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APK_FILENAME="${PKG_NAME}-${PKG_VERSION}-r${PKG_RELEASE}.apk" + +if ! command -v docker &>/dev/null; then + echo " [SKIP] Docker not found — APK build requires Docker with Alpine" + echo " IPK package was built successfully." +else + # Prepare post-install script for APK + APK_SCRIPT="$WORK_DIR/post-install.sh" + cat > "$APK_SCRIPT" <<'SCRIPT' +#!/bin/sh +uci get luci.themes.Glass >/dev/null 2>&1 || \ +uci batch <<-EOF + set luci.themes.Glass=/luci-static/glass + set luci.main.mediaurlbase=/luci-static/glass + commit luci +EOF +exit 0 +SCRIPT + chmod 755 "$APK_SCRIPT" + + # Build using Alpine container with apk mkpkg + docker run --rm \ + -v "$DATA_DIR:/pkg/files:ro" \ + -v "$APK_SCRIPT:/pkg/post-install.sh:ro" \ + -v "$SCRIPT_DIR/$DIST_DIR:/pkg/out" \ + alpine:latest sh -c " + apk add --no-cache apk-tools-mkpkg >/dev/null 2>&1 || true + apk mkpkg \ + --info 'name:$PKG_NAME' \ + --info 'version:${PKG_VERSION}-r${PKG_RELEASE}' \ + --info 'description:$DESCRIPTION' \ + --info 'arch:noarch' \ + --info 'license:$LICENSE' \ + --info 'origin:$PKG_NAME' \ + --info 'url:$HOMEPAGE' \ + --info 'maintainer:$MAINTAINER' \ + --script 'post-install:/pkg/post-install.sh' \ + --files /pkg/files \ + --output /pkg/out/$APK_FILENAME + " 2>&1 + + echo " -> $DIST_DIR/$APK_FILENAME" +fi + +echo "" +echo "==> Done! Packages in $DIST_DIR/" +ls -lh "$DIST_DIR/" diff --git a/luci-theme-glass/htdocs/luci-static/glass/background/.gitkeep b/luci-theme-glass/htdocs/luci-static/glass/background/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/luci-theme-glass/htdocs/luci-static/glass/css/cascade.css b/luci-theme-glass/htdocs/luci-static/glass/css/cascade.css new file mode 100644 index 000000000..ab274ba9c --- /dev/null +++ b/luci-theme-glass/htdocs/luci-static/glass/css/cascade.css @@ -0,0 +1,2591 @@ +/* ========================================================================== + luci-theme-glass — Master Stylesheet + Import order matters: tokens → reset → mixins → layout → components → fixes + ========================================================================== */ +/* ========================================================================== + Design Tokens — luci-theme-glass + All CSS custom properties live here for easy theming. + ========================================================================== */ +:root { + /* ---- Typography ---- */ + --font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", "Segoe UI", Roboto, "Noto Sans", sans-serif; + --font-mono: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, "Liberation Mono", monospace; + --font-size-base: 0.875rem; + --font-size-sm: 0.8125rem; + --font-size-lg: 1rem; + --font-size-xl: 1.25rem; + --font-size-2xl: 1.75rem; + --line-height: 1.5; + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + /* ---- Colors — Light mode ---- */ + --color-primary: #007AFF; + --color-primary-hover: #0066d6; + --color-primary-active: #004fad; + --color-primary-rgb: 0, 122, 255; + --color-success: #34C759; + --color-warning: #FF9500; + --color-danger: #FF3B30; + --color-info: #5AC8FA; + --color-text: #1d1d1f; + --color-text-secondary: #6e6e73; + --color-text-tertiary: #aeaeb2; + --color-text-inverse: #ffffff; + --color-bg: #f5f5f7; + --color-bg-elevated: #ffffff; + --color-bg-secondary: #e8e8ed; + --color-border: rgba(0, 0, 0, 0.08); + --color-border-strong: rgba(0, 0, 0, 0.16); + --color-separator: rgba(60, 60, 67, 0.12); + /* ---- Glassmorphism ---- */ + --glass-blur: 20px; + --glass-saturate: 180%; + --glass-bg: rgba(255, 255, 255, 0.72); + --glass-bg-hover: rgba(255, 255, 255, 0.82); + --glass-bg-strong: rgba(255, 255, 255, 0.85); + --glass-border: rgba(255, 255, 255, 0.45); + --glass-shadow: 0 2px 16px rgba(0, 0, 0, 0.06), 0 0 1px rgba(0, 0, 0, 0.08); + --glass-shadow-lg: 0 8px 40px rgba(0, 0, 0, 0.1), 0 0 1px rgba(0, 0, 0, 0.06); + /* ---- Layout ---- */ + --sidebar-width: 260px; + --sidebar-collapsed-width: 64px; + --sidebar-gap: 10px; + --header-height: 56px; + --content-max-width: none; + --content-padding: 24px; + /* ---- Radius ---- */ + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 14px; + --radius-xl: 20px; + --radius-full: 9999px; + /* ---- Spacing ---- */ + --space-xs: 4px; + --space-sm: 8px; + --space-md: 12px; + --space-lg: 16px; + --space-xl: 24px; + --space-2xl: 32px; + --space-3xl: 48px; + /* ---- Transitions ---- */ + --transition-fast: 0.15s ease; + --transition-normal: 0.25s ease; + --transition-slow: 0.4s cubic-bezier(0.16, 1, 0.3, 1); + /* ---- Z-index ---- */ + --z-dropdown: 100; + --z-sticky: 200; + --z-sidebar: 300; + --z-header: 400; + --z-overlay: 500; + --z-modal: 600; + --z-toast: 700; + /* ---- Background ---- */ + --bg-gradient: linear-gradient(135deg, #b8c4d0 0%, #d5dce5 50%, #ddd5cc 100%); +} +/* ========================================================================== + Normalize / Reset — luci-theme-glass + Minimal reset for LuCI pages. + ========================================================================== */ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} +html { + font-size: var(--font-size-base); + line-height: var(--line-height); + -webkit-text-size-adjust: 100%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + /* Glass scrollbar */ + scrollbar-width: thin; + scrollbar-color: rgba(128, 128, 128, 0.35) transparent; +} +::-webkit-scrollbar { + width: 6px; + height: 6px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: rgba(128, 128, 128, 0.35); + border-radius: 3px; +} +::-webkit-scrollbar-thumb:hover { + background: rgba(128, 128, 128, 0.55); +} +body { + font-family: var(--font-sans); + font-size: var(--font-size-base); + color: var(--color-text); + background: var(--color-bg); + min-height: 100vh; + overflow-x: hidden; +} +a { + color: var(--color-primary); + text-decoration: none; + transition: color var(--transition-fast); +} +a:hover { + color: var(--color-primary-hover); +} +img { + max-width: 100%; + height: auto; +} +table { + border-collapse: collapse; + border-spacing: 0; + width: 100%; +} +input, +select, +textarea, +button { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} +code, +pre { + font-family: var(--font-mono); + font-size: var(--font-size-sm); +} +hr { + border: none; + border-top: 0.5px solid var(--color-border); + margin: var(--space-md) 0; +} +p { + margin-bottom: var(--space-sm); +} +ul, +ol { + padding-left: 1.5em; + margin: var(--space-sm) 0; +} +li { + margin-bottom: 2px; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-weight: var(--font-weight-semibold); + line-height: 1.25; + color: var(--color-text); +} +h1 { + font-size: var(--font-size-2xl); +} +h2 { + font-size: var(--font-size-xl); +} +h3 { + font-size: 1.125rem; +} +h4 { + font-size: 1rem; +} +::selection { + background: rgba(var(--color-primary-rgb), 0.25); +} +/* LuCI injects at the top of every + CBI form. Without this rule the input renders as an unstyled browser + button (seen in PassWall, OpenAppFilter, etc). Both bootstrap and argon + themes ship this utility rule. */ +.hidden { + display: none; +} +/* ========================================================================== + Glassmorphism Mixins — luci-theme-glass + ========================================================================== */ +/* Standard glass panel — default for cards, sections, dropdowns */ +/* Frosted glass — stronger blur, higher opacity (sidebar, login card) */ +/* Subtle glass — minimal effect for secondary/nested elements */ +/* Glass hover state */ +/* Inner glow — top highlight for glass panels */ +/* ========================================================================== + Layout — Sidebar, Header, Content + ========================================================================== */ +/* ---- Background layer ---- */ +#bg-wrap { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + background: var(--bg-gradient); + background-size: cover; + background-position: center; +} +#bg-wrap img, +#bg-wrap video { + width: 100%; + height: 100%; + object-fit: cover; +} +/* ---- Sidebar ---- */ +#sidebar { + position: fixed; + top: var(--sidebar-gap); + left: var(--sidebar-gap); + width: var(--sidebar-width); + height: calc(100vh - var(--sidebar-gap) * 2); + z-index: var(--z-sidebar); + /* True glassmorphism — multi-stop gradient, stronger for readability */ + background-image: linear-gradient(160deg, rgba(255, 255, 255, 0.22) 0%, rgba(255, 255, 255, 0.18) 20%, rgba(245, 250, 255, 0.2) 45%, rgba(255, 255, 255, 0.16) 70%, rgba(240, 245, 255, 0.19) 100%); + background-color: rgba(255, 255, 255, 0.62); + backdrop-filter: blur(30px) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(30px) saturate(var(--glass-saturate)); + border: 0.5px solid rgba(255, 255, 255, 0.4); + box-shadow: 0 8px 40px rgba(0, 0, 0, 0.08), 0 2px 8px rgba(0, 0, 0, 0.04), inset 0 1px 0 rgba(255, 255, 255, 0.5), inset 0 0 12px rgba(255, 255, 255, 0.15); + border-radius: var(--radius-xl); + display: flex; + flex-direction: column; + transition: transform var(--transition-slow); + overflow: hidden; +} +.sidebar-header { + display: flex; + align-items: center; + padding: var(--space-lg) var(--space-xl); + height: var(--header-height); + border-bottom: 0.5px solid var(--color-separator); +} +.sidebar-header .logo { + width: 32px; + height: 32px; + flex-shrink: 0; +} +.sidebar-header .brand { + margin-left: var(--space-md); + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semibold); + color: var(--color-text); + white-space: nowrap; + overflow: hidden; +} +.sidebar-footer { + padding: var(--space-md) var(--space-xl); + border-top: 0.5px solid var(--color-separator); + text-align: center; +} +.sidebar-footer .sidebar-version { + font-size: 0.75rem; + color: var(--color-text-tertiary); +} +/* ---- Sidebar navigation ---- */ +.sidebar-nav { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: var(--space-sm) 0; +} +.sidebar-nav::-webkit-scrollbar { + width: 4px; +} +.sidebar-nav::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.15); + border-radius: var(--radius-full); +} +.nav-group { + margin-bottom: var(--space-xs); +} +.nav-group .nav-group-title { + padding: var(--space-sm) var(--space-xl); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + color: var(--color-text-tertiary); + text-transform: uppercase; + letter-spacing: 0.05em; +} +.nav-item { + display: flex; + align-items: center; + padding: 0.55rem var(--space-xl); + margin: 1px var(--space-sm); + border-radius: var(--radius-md); + font-size: var(--font-size-lg); + color: var(--color-text); + cursor: pointer; + transition: background var(--transition-fast), color var(--transition-fast); + text-decoration: none; +} +.nav-item:hover { + background: rgba(0, 0, 0, 0.04); + color: var(--color-text); +} +.nav-item.active { + background: rgba(255, 255, 255, 0.45); + color: var(--color-text); + font-weight: var(--font-weight-medium); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), inset 0 1px 1px rgba(255, 255, 255, 0.6), inset 0 0 6px rgba(255, 255, 255, 0.15); +} +.nav-item .nav-icon { + width: 20px; + height: 20px; + margin-right: var(--space-md); + flex-shrink: 0; + opacity: 0.65; +} +.nav-item.active .nav-icon { + opacity: 1; +} +.nav-item .nav-label { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.nav-item .nav-arrow { + margin-left: auto; + width: 16px; + height: 16px; + opacity: 0.4; + transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), opacity var(--transition-fast); +} +.nav-item.open .nav-arrow { + transform: rotate(90deg); + opacity: 0.6; +} +.nav-sub { + position: relative; + overflow: hidden; + max-height: 0; + opacity: 0; + transition: max-height 0.35s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.25s ease; +} +.nav-sub.open { + /* Upper bound for the expand animation. Must exceed the tallest submenu + encountered in the wild (System commonly reaches ~14 items once third- + party apps like theme-config, scheduled-tasks, etc. are installed). */ + max-height: 2000px; + opacity: 1; +} +.nav-sub .nav-item { + padding-left: calc(var(--space-xl) + 32px); + font-size: var(--font-size-base); + transform: translateX(-8px); + opacity: 0; + transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.2s ease, color var(--transition-fast); + /* Sub-item active state — no background, slider handles it */ +} +.nav-sub .nav-item.active { + background: transparent; + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; + font-weight: var(--font-weight-medium); +} +.nav-sub.open .nav-item { + transform: translateX(0); + opacity: 1; +} +.nav-sub.open .nav-item:nth-of-type(1) { + transition-delay: 0.03s; +} +.nav-sub.open .nav-item:nth-of-type(2) { + transition-delay: 0.06s; +} +.nav-sub.open .nav-item:nth-of-type(3) { + transition-delay: 0.09s; +} +.nav-sub.open .nav-item:nth-of-type(4) { + transition-delay: 0.12s; +} +.nav-sub.open .nav-item:nth-of-type(5) { + transition-delay: 0.15s; +} +.nav-sub.open .nav-item:nth-of-type(6) { + transition-delay: 0.18s; +} +.nav-sub.open .nav-item:nth-of-type(7) { + transition-delay: 0.21s; +} +.nav-sub.open .nav-item:nth-of-type(8) { + transition-delay: 0.24s; +} +.nav-sub.open .nav-item:nth-of-type(n+9) { + transition-delay: 0.27s; +} +.nav-sub .nav-slider { + position: absolute; + left: var(--space-sm); + right: var(--space-sm); + top: 0; + height: 0; + opacity: 0; + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.35); + backdrop-filter: blur(12px) saturate(150%); + -webkit-backdrop-filter: blur(12px) saturate(150%); + border: 0.5px solid rgba(255, 255, 255, 0.45); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), inset 0 1px 1px rgba(255, 255, 255, 0.35), inset 0 0 4px rgba(255, 255, 255, 0.1); + transition: top 0.3s cubic-bezier(0.16, 1, 0.3, 1), height 0.2s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.2s ease; + z-index: 0; + pointer-events: none; +} +.nav-sub .nav-item { + position: relative; + z-index: 1; +} +/* ---- Sidebar user area ---- */ +.sidebar-user { + padding: var(--space-lg) var(--space-xl); + border-top: 0.5px solid var(--color-separator); + display: flex; + align-items: center; +} +.sidebar-user .user-avatar { + width: 32px; + height: 32px; + border-radius: var(--radius-full); + background: var(--color-bg-secondary); + display: flex; + align-items: center; + justify-content: center; + font-weight: var(--font-weight-semibold); + font-size: var(--font-size-sm); + color: var(--color-text-secondary); + flex-shrink: 0; +} +.sidebar-user .user-info { + margin-left: var(--space-md); + overflow: hidden; +} +.sidebar-user .user-info .user-name { + font-weight: var(--font-weight-medium); + font-size: var(--font-size-sm); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.sidebar-user .user-info .user-host { + font-size: var(--font-size-sm); + color: var(--color-text-tertiary); +} +/* ---- Top header nav bar ---- */ +#header { + position: fixed; + top: var(--sidebar-gap); + left: calc(var(--sidebar-width) + var(--sidebar-gap) * 2); + right: var(--sidebar-gap); + height: 44px; + z-index: var(--z-header); + border-radius: var(--radius-lg); + display: flex; + align-items: center; + padding: 4px 4px 4px 6px; + gap: var(--space-xs); + transition: left var(--transition-slow); + /* True glassmorphism — multi-stop gradient like sub-nav */ + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.12) 25%, rgba(240, 248, 255, 0.13) 50%, rgba(255, 255, 255, 0.11) 75%, rgba(230, 240, 255, 0.12) 100%); + background-color: transparent; + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + border: 0.5px solid rgba(255, 255, 255, 0.35); + box-shadow: 0 2px 16px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.03), inset 0 1px 0 rgba(255, 255, 255, 0.4), inset 0 0 8px rgba(255, 255, 255, 0.12); +} +/* ---- Header top-level tabs ---- */ +#header-nav, +#header-tabs { + display: flex; + align-items: center; + position: relative; +} +.header-sep { + width: 1px; + height: 18px; + background: var(--color-separator); + margin: 0 var(--space-xs); + flex-shrink: 0; +} +.header-tab { + padding: 6px var(--space-lg); + border-radius: var(--radius-md); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + text-decoration: none; + transition: background var(--transition-fast), color var(--transition-fast), box-shadow var(--transition-fast); + white-space: nowrap; +} +.header-tab:hover { + background: rgba(0, 0, 0, 0.04); + color: var(--color-text); +} +.header-tab.active { + color: var(--color-text); + background: transparent; + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; +} +/* Horizontal sliding selector for header tabs */ +.tab-slider { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 0; + opacity: 0; + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.45); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), inset 0 1px 1px rgba(255, 255, 255, 0.6), inset 0 0 6px rgba(255, 255, 255, 0.15); + transition: left 0.3s cubic-bezier(0.16, 1, 0.3, 1), width 0.2s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.2s ease; + z-index: 0; + pointer-events: none; +} +.header-tab, +.sub-tab { + position: relative; + z-index: 1; +} +/* ---- Status indicators (center of header) ---- */ +#status-indicators { + margin: 0 auto; + gap: var(--space-sm); + padding: 4px var(--space-md); + height: 2.25rem; + display: flex; + align-items: center; + border-radius: var(--radius-md); + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.1) 25%, rgba(240, 248, 255, 0.12) 50%, rgba(255, 255, 255, 0.08) 75%, rgba(230, 240, 255, 0.1) 100%); + background-color: transparent; + border: 0.5px solid rgba(255, 255, 255, 0.18); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.03), 0 0 0 0.5px rgba(255, 255, 255, 0.08) inset, 0 1px 6px rgba(255, 255, 255, 0.1) inset; + backdrop-filter: blur(12px) saturate(180%); + -webkit-backdrop-filter: blur(12px) saturate(180%); +} +#status-indicators:empty { + display: none; +} +#status-indicators [data-indicator] { + display: flex; + align-items: center; + gap: 4px; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + white-space: nowrap; + cursor: default; + transition: color 0.3s ease; + /* Level colors — neutral by default, color only when noteworthy */ +} +#status-indicators [data-indicator] .indicator-icon { + width: 14px; + height: 14px; + flex-shrink: 0; + opacity: 0.6; + transition: color 0.3s ease, opacity 0.3s ease; +} +#status-indicators [data-indicator][data-level="warn"] { + color: #FF9500; +} +#status-indicators [data-indicator][data-level="warn"] .indicator-icon { + opacity: 0.85; +} +#status-indicators [data-indicator][data-level="crit"] { + color: #FF3B30; +} +#status-indicators [data-indicator][data-level="crit"] .indicator-icon { + opacity: 0.9; +} +#status-indicators [data-indicator][data-level="active"] { + color: #007AFF; +} +#status-indicators [data-indicator][data-level="active"] .indicator-icon { + opacity: 0.8; +} +#status-indicators [data-indicator][data-level="busy"] { + color: #AF52DE; +} +#status-indicators [data-indicator][data-level="busy"] .indicator-icon { + opacity: 0.85; +} +#status-indicators [data-indicator] + [data-indicator] { + border-left: 0.5px solid rgba(128, 128, 128, 0.25); + padding-left: var(--space-sm); +} +/* ---- Glass pill badges (title + indicators) ---- */ +.header-title { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + color: var(--color-text); + white-space: nowrap; + cursor: default; + padding: 4px var(--space-md); + height: 2.25rem; + display: flex; + align-items: center; + border-radius: var(--radius-md); + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.1) 25%, rgba(240, 248, 255, 0.12) 50%, rgba(255, 255, 255, 0.08) 75%, rgba(230, 240, 255, 0.1) 100%); + background-color: transparent; + border: 0.5px solid rgba(255, 255, 255, 0.18); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.03), 0 0 0 0.5px rgba(255, 255, 255, 0.08) inset, 0 1px 6px rgba(255, 255, 255, 0.1) inset; + backdrop-filter: blur(12px) saturate(180%); + -webkit-backdrop-filter: blur(12px) saturate(180%); +} +.header-actions { + margin-left: auto; + flex-shrink: 0; + gap: var(--space-sm); + padding: 4px var(--space-md); + height: 2.25rem; + display: flex; + align-items: center; + border-radius: var(--radius-md); + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.1) 25%, rgba(240, 248, 255, 0.12) 50%, rgba(255, 255, 255, 0.08) 75%, rgba(230, 240, 255, 0.1) 100%); + background-color: transparent; + border: 0.5px solid rgba(255, 255, 255, 0.18); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.03), 0 0 0 0.5px rgba(255, 255, 255, 0.08) inset, 0 1px 6px rgba(255, 255, 255, 0.1) inset; + backdrop-filter: blur(12px) saturate(180%); + -webkit-backdrop-filter: blur(12px) saturate(180%); +} +.header-actions:empty { + display: none; +} +.header-actions [data-indicator] { + display: flex; + align-items: center; + gap: 4px; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + white-space: nowrap; + cursor: default; +} +.header-actions [data-indicator][data-style="active"] { + color: var(--color-primary); +} +/* ---- Theme toggle button ---- */ +.theme-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + border: none; + border-radius: var(--radius-sm); + background: none; + color: var(--color-text-secondary); + cursor: pointer; + transition: color var(--transition-fast); + flex-shrink: 0; +} +.theme-toggle:hover { + color: var(--color-text); +} +.theme-toggle:active { + transform: scale(0.92); +} +.theme-toggle .theme-icon { + width: 15px; + height: 15px; + display: none; +} +/* Show only the icon matching current theme */ +html[data-theme="auto"] .theme-icon-auto, +html:not([data-theme]) .theme-icon-auto { + display: block; +} +html[data-theme="light"] .theme-icon-light { + display: block; +} +html[data-theme="dark"] .theme-icon-dark { + display: block; +} +/* Floating variant for login page */ +.theme-toggle-float { + position: fixed; + top: var(--space-lg); + right: var(--space-lg); + z-index: var(--z-dropdown); + width: 36px; + height: 36px; + height: 2.25rem; + display: flex; + align-items: center; + border-radius: var(--radius-md); + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.1) 25%, rgba(240, 248, 255, 0.12) 50%, rgba(255, 255, 255, 0.08) 75%, rgba(230, 240, 255, 0.1) 100%); + background-color: transparent; + border: 0.5px solid rgba(255, 255, 255, 0.18); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.03), 0 0 0 0.5px rgba(255, 255, 255, 0.08) inset, 0 1px 6px rgba(255, 255, 255, 0.1) inset; + backdrop-filter: blur(12px) saturate(180%); + -webkit-backdrop-filter: blur(12px) saturate(180%); +} +.theme-toggle-float .theme-icon { + width: 18px; + height: 18px; +} +/* Mobile hamburger */ +.menu-toggle { + display: none; + background: none; + border: none; + cursor: pointer; + padding: var(--space-sm); + margin-right: var(--space-md); + color: var(--color-text); +} +.menu-toggle svg { + width: 22px; + height: 22px; +} +/* ---- Sub-navigation bar (CBI tabs) ---- */ +#header-sub { + position: fixed; + top: calc(var(--sidebar-gap) + 44px + 4px); + left: calc(var(--sidebar-width) + var(--sidebar-gap) * 2); + width: fit-content; + max-width: calc(100vw - var(--sidebar-width) - var(--sidebar-gap) * 3); + height: 2.25rem; + z-index: var(--z-header); + border-radius: var(--radius-md); + display: none; + align-items: center; + padding: 3px 4px; + gap: 2px; + overflow-x: auto; + transition: left var(--transition-slow); + /* Glass tab-list background — from glass-ui */ + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.12) 25%, rgba(240, 248, 255, 0.13) 50%, rgba(255, 255, 255, 0.11) 75%, rgba(230, 240, 255, 0.12) 100%); + background-color: transparent; + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + border: 0.5px solid rgba(255, 255, 255, 0.12); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08), 0 1px 4px rgba(0, 0, 0, 0.04), 0 0 0 0.5px rgba(255, 255, 255, 0.08) inset, 0 0 12px rgba(255, 255, 255, 0.1) inset; + /* Hide scrollbar */ + scrollbar-width: none; +} +#header-sub::-webkit-scrollbar { + display: none; +} +#header-sub.active { + display: inline-flex; +} +/* Sub-nav tab items — glass-ui TabsTrigger pattern */ +.sub-tab { + padding: 4px var(--space-md); + border-radius: var(--radius-sm); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + text-decoration: none; + white-space: nowrap; + cursor: pointer; + transition: background var(--transition-fast), color var(--transition-fast), box-shadow var(--transition-fast), backdrop-filter var(--transition-fast); +} +.sub-tab:hover { + background: rgba(0, 0, 0, 0.04); + color: var(--color-text); +} +.sub-tab.active { + color: var(--color-text); + background: transparent; + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; +} +/* Sub-tab slider — inset to match sub-tab content area */ +#header-sub > .tab-slider { + border-radius: var(--radius-sm); + top: 3px; + bottom: 3px; + height: auto; +} +/* ---- Page transition ---- */ +@keyframes page-enter { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} +/* ---- Main content area ---- */ +#content-wrap { + margin-left: calc(var(--sidebar-width) + var(--sidebar-gap) * 2); + margin-top: calc(44px + var(--sidebar-gap) * 2); + min-height: calc(100vh - 44px - var(--sidebar-gap) * 2); + padding: var(--content-padding); + transition: margin-left var(--transition-slow); + animation: page-enter 0.3s cubic-bezier(0.16, 1, 0.3, 1) both; +} +/* JS view content — fade in when LuCI replaces the spinner */ +#view > :not(.spinning) { + animation: page-enter 0.25s cubic-bezier(0.16, 1, 0.3, 1) both; +} +/* Style LuCI's "Loading view..." spinner as a centered glass card */ +#view > .spinning { + position: fixed; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); + padding: var(--space-lg) var(--space-xl); + border-radius: var(--radius-lg); + background: var(--glass-bg); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + border: 0.5px solid var(--glass-border); + box-shadow: var(--glass-shadow); + z-index: 100; +} +/* Fade-out before navigation */ +body.page-leaving #content-wrap { + opacity: 0; + transform: translateY(-8px); + transition: opacity 0.15s ease, transform 0.15s ease; +} +/* Push content down when sub-nav is visible */ +body.has-sub-nav #content-wrap { + margin-top: calc(44px + 36px + var(--sidebar-gap) * 2 + 4px); +} +#content { + max-width: var(--content-max-width); + margin: 0 auto; +} +/* Mobile overlay */ +.sidebar-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.3); + z-index: calc(var(--z-sidebar) - 1); + opacity: 0; + transition: opacity var(--transition-normal); +} +.sidebar-overlay.active { + display: block; + opacity: 1; +} +/* ---- Page title ---- */ +.page-title { + margin-bottom: var(--space-xl); +} +.page-title h2 { + font-size: var(--font-size-xl); + font-weight: var(--font-weight-bold); +} +.page-title .page-description { + margin-top: var(--space-xs); + color: var(--color-text-secondary); + font-size: var(--font-size-sm); +} +/* ---- Footer ---- */ +.glass-footer { + margin-left: calc(var(--sidebar-width) + var(--sidebar-gap) * 2); + padding: var(--space-lg) var(--content-padding); + text-align: right; + font-size: var(--font-size-sm); + color: var(--color-text-tertiary); + transition: margin-left var(--transition-slow); +} +.glass-footer a { + color: var(--color-text-tertiary); + text-decoration: none; +} +.glass-footer a:hover { + color: var(--color-text-secondary); +} +.glass-footer .footer-sep { + margin: 0 var(--space-sm); + opacity: 0.4; +} +/* ========================================================================== + Components — CBI forms, tables, buttons, inputs, dropdowns + ========================================================================== */ +/* ---- CBI Sections ---- */ +.cbi-section { + background: var(--glass-bg); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + border: 0.5px solid var(--glass-border); + box-shadow: var(--glass-shadow); + border-radius: var(--radius-lg); + padding: var(--space-xl); + margin-bottom: var(--space-xl); + position: relative; + overflow-x: auto; + /* Lift section above siblings when a dropdown is open inside it */ + /* Nested sections — flatten (no extra box/padding) */ +} +.cbi-section:has(.cbi-dropdown[open]) { + z-index: var(--z-dropdown); +} +.cbi-section .cbi-section { + padding: 0; + margin-bottom: 0; + background: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; + border: none; + box-shadow: none; + border-radius: 0; +} +/* Allow dropdowns to escape stacking contexts created by backdrop-filter. + Temporarily remove glass effects on the section and force overflow visible + on all ancestors up to the section. */ +.cbi-section:has(.cbi-dropdown[open]) { + z-index: var(--z-modal); + backdrop-filter: none; + -webkit-backdrop-filter: none; + overflow: visible; +} +.cbi-section:has(.cbi-dropdown[open]) .table, +.cbi-section:has(.cbi-dropdown[open]) .tr, +.cbi-section:has(.cbi-dropdown[open]) .td, +.cbi-section:has(.cbi-dropdown[open]) .cbi-value, +.cbi-section:has(.cbi-dropdown[open]) .cbi-value-field { + overflow: visible !important; +} +.cbi-section-descr { + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + margin-bottom: var(--space-lg); + padding-bottom: var(--space-lg); + border-bottom: 0.5px solid var(--color-separator); +} +.cbi-section > h3, +.cbi-section-node > h3 { + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semibold); + margin-bottom: var(--space-lg); +} +/* ---- Section fold/collapse ---- */ +.cbi-section.fade-in .cbi-title { + display: flex; + align-items: center; + /* Move toggle button to the right of the title */ +} +.cbi-section.fade-in .cbi-title > h3 { + flex: 1; + margin-bottom: 0; +} +.cbi-section.fade-in .cbi-title > div:last-child { + flex-shrink: 0; + margin-left: var(--space-md); +} +.cbi-section.fade-in .cbi-title > div:last-child span { + font-size: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + cursor: pointer; + border-radius: var(--radius-sm); + transition: background var(--transition-fast); +} +.cbi-section.fade-in .cbi-title > div:last-child span:hover { + background: rgba(0, 0, 0, 0.06); +} +.cbi-section.fade-in .cbi-title > div:last-child span::after { + content: ''; + display: block; + width: 7px; + height: 7px; + border-right: 1.5px solid var(--color-text-tertiary); + border-bottom: 1.5px solid var(--color-text-tertiary); + transform: rotate(-135deg); + transition: transform 0.3s ease; + margin-top: 3px; +} +.cbi-section.fade-in .cbi-title > div:last-child span[data-style="inactive"]::after { + transform: rotate(45deg); + margin-top: -2px; +} +/* ---- CBI Map (page-level container) ---- */ +.cbi-map { + margin-bottom: var(--space-xl); +} +/* Title shown in header bar; description becomes tooltip */ +.cbi-map-descr, +.cbi-map > h2, +#view > h2, +#maincontent > h2 { + display: none; +} +/* Hide descriptions only when moved to header tooltip (data-moved set by footer.ut JS) */ +.cbi-map-descr[data-moved], +#view > h2 + [data-moved] { + display: none; +} +/* ---- CBI Value rows ---- */ +.cbi-value { + display: flex; + align-items: flex-start; + padding: var(--space-md) 0; + border-bottom: 0.5px solid var(--color-separator); + /* Plugins that use form.DummyValue.extend({ renderWidget }) to emit a + full-width section or table (usteer Hearing map, DHCP leases, etc.) + render the section as the *only* child of .cbi-value — no title, no + value-field wrapper. As a flex item with default `flex: 0 1 auto` it + shrinks to content width, leaving a big empty right zone + ([#8](https://github.com/rchen14b/luci-theme-glass/issues/8)). Switch + the row to block so the section spans full width. */ +} +.cbi-value:last-child { + border-bottom: none; +} +.cbi-value:has(> .cbi-section), +.cbi-value:has(> table) { + display: block; +} +.cbi-value-title { + flex: 0 0 200px; + padding-right: var(--space-xl); + font-weight: var(--font-weight-medium); + padding-top: var(--space-sm); + color: var(--color-text); +} +.cbi-value-description { + color: var(--color-text-tertiary); + font-size: var(--font-size-sm); + margin-top: var(--space-xs); + font-weight: var(--font-weight-normal); +} +.cbi-value-field { + flex: 1; + min-width: 0; +} +/* ---- Inputs ---- */ +.cbi-input-text, +.cbi-input-password, +input[type="text"], +input[type="password"], +input[type="number"], +textarea { + width: 100%; + max-width: 400px; + padding: var(--space-sm) var(--space-md); + background: rgba(255, 255, 255, 0.55); + border: 1px solid var(--color-border-strong); + border-radius: var(--radius-md); + font-size: var(--font-size-base); + color: var(--color-text); + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); + outline: none; +} +.cbi-input-text:focus, +.cbi-input-password:focus, +input[type="text"]:focus, +input[type="password"]:focus, +input[type="number"]:focus, +textarea:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb), 0.15); +} +.cbi-input-text:disabled, +.cbi-input-password:disabled, +input[type="text"]:disabled, +input[type="password"]:disabled, +input[type="number"]:disabled, +textarea:disabled { + opacity: 0.5; + cursor: not-allowed; +} +textarea { + max-width: 100%; + min-height: 80px; + resize: vertical; +} +select { + padding: var(--space-sm) var(--space-md); + background: rgba(255, 255, 255, 0.55); + border: 1px solid var(--color-border-strong); + border-radius: var(--radius-md); + font-size: var(--font-size-base); + color: var(--color-text); + outline: none; + cursor: pointer; +} +select:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb), 0.15); +} +/* ---- Buttons — glass-ui pattern ---- */ +.btn, +.cbi-button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: var(--space-sm) var(--space-lg); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + border-radius: var(--radius-md); + /* Glass button surface */ + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.12) 25%, rgba(240, 248, 255, 0.13) 50%, rgba(255, 255, 255, 0.11) 75%, rgba(230, 240, 255, 0.12) 100%); + background-color: rgba(255, 255, 255, 0.55); + border: 0.5px solid rgba(255, 255, 255, 0.3); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04), inset 0 1px 0 rgba(255, 255, 255, 0.4), inset 0 0 6px rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px) saturate(180%); + -webkit-backdrop-filter: blur(10px) saturate(180%); + color: var(--color-text); + cursor: pointer; + transition: all var(--transition-fast); + text-decoration: none; + white-space: nowrap; + gap: var(--space-xs); +} +.btn:hover, +.cbi-button:hover { + opacity: 0.9; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.45), inset 0 0 8px rgba(255, 255, 255, 0.12); +} +.btn:active, +.cbi-button:active { + transform: scale(0.98); + opacity: 0.85; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), inset 0 1px 2px rgba(0, 0, 0, 0.1); +} +.btn:disabled, +.cbi-button:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} +.cbi-button-positive, +.cbi-button-apply, +.cbi-button-action, +.cbi-button-reload, +.cbi-button[value="Submit"], +.cbi-button[value="Upload"], +.cbi-button[value$="Apply"] { + /* Primary glass — blue-tinted (Save & Apply, actions) */ + background-image: linear-gradient(135deg, rgba(0, 122, 255, 0.1) 0%, rgba(0, 122, 255, 0.07) 25%, rgba(10, 132, 255, 0.09) 50%, rgba(0, 122, 255, 0.06) 75%, rgba(0, 100, 220, 0.08) 100%); + background-color: rgba(255, 255, 255, 0.45); + color: var(--color-primary); + border: 0.5px solid rgba(0, 122, 255, 0.18); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04), inset 0 1px 0 rgba(255, 255, 255, 0.35), inset 0 0 6px rgba(255, 255, 255, 0.08); +} +.cbi-button-positive:hover, +.cbi-button-apply:hover, +.cbi-button-action:hover, +.cbi-button-reload:hover, +.cbi-button[value="Submit"]:hover, +.cbi-button[value="Upload"]:hover, +.cbi-button[value$="Apply"]:hover { + opacity: 0.9; + color: var(--color-primary); + box-shadow: 0 4px 12px rgba(0, 122, 255, 0.12), 0 1px 3px rgba(0, 0, 0, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.4), inset 0 0 8px rgba(255, 255, 255, 0.1); +} +.cbi-button-positive:active, +.cbi-button-apply:active, +.cbi-button-action:active, +.cbi-button-reload:active, +.cbi-button[value="Submit"]:active, +.cbi-button[value="Upload"]:active, +.cbi-button[value$="Apply"]:active { + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), inset 0 1px 2px rgba(0, 0, 0, 0.1); +} +.cbi-button-save { + /* Secondary glass — green-tinted (Save) */ + background-image: linear-gradient(135deg, rgba(52, 199, 89, 0.1) 0%, rgba(52, 199, 89, 0.07) 25%, rgba(48, 209, 88, 0.09) 50%, rgba(52, 199, 89, 0.06) 75%, rgba(40, 180, 75, 0.08) 100%); + background-color: rgba(255, 255, 255, 0.45); + color: #248a3d; + border: 0.5px solid rgba(52, 199, 89, 0.2); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04), inset 0 1px 0 rgba(255, 255, 255, 0.35), inset 0 0 6px rgba(255, 255, 255, 0.08); +} +.cbi-button-save:hover { + opacity: 0.9; + color: #248a3d; + box-shadow: 0 4px 12px rgba(52, 199, 89, 0.12), 0 1px 3px rgba(0, 0, 0, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.4), inset 0 0 8px rgba(255, 255, 255, 0.1); +} +.cbi-button-save:active { + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), inset 0 1px 2px rgba(0, 0, 0, 0.1); +} +.cbi-button-negative, +.cbi-button-remove, +.cbi-button-reset { + /* Tinted danger glass */ + background-image: linear-gradient(135deg, rgba(255, 59, 48, 0.1) 0%, rgba(255, 59, 48, 0.07) 50%, rgba(255, 59, 48, 0.09) 100%); + background-color: rgba(255, 255, 255, 0.45); + color: var(--color-danger); + border: 0.5px solid rgba(255, 59, 48, 0.18); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.03), inset 0 1px 0 rgba(255, 255, 255, 0.35), inset 0 0 6px rgba(255, 255, 255, 0.08); +} +.cbi-button-negative:hover, +.cbi-button-remove:hover, +.cbi-button-reset:hover { + background-color: rgba(255, 255, 255, 0.5); + box-shadow: 0 4px 12px rgba(255, 59, 48, 0.12), 0 1px 3px rgba(0, 0, 0, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.4), inset 0 0 8px rgba(255, 255, 255, 0.1); +} +.cbi-button-negative:active, +.cbi-button-remove:active, +.cbi-button-reset:active { + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), inset 0 1px 2px rgba(0, 0, 0, 0.1); +} +.cbi-button-neutral, +.cbi-button-link, +.cbi-button-edit, +.cbi-button-find, +.cbi-button-add, +.cbi-button-download, +.cbi-button-up, +.cbi-button-down { + background-color: rgba(255, 255, 255, 0.4); + color: var(--color-text); +} +.cbi-page-actions { + border-radius: var(--radius-lg); + padding: var(--space-md) var(--space-xl); + margin-top: var(--space-xl); + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--space-sm); + /* Match header nav bar glass style */ + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.12) 25%, rgba(240, 248, 255, 0.13) 50%, rgba(255, 255, 255, 0.11) 75%, rgba(230, 240, 255, 0.12) 100%); + background-color: transparent; + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + border: 0.5px solid rgba(255, 255, 255, 0.35); + box-shadow: 0 2px 16px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.03), inset 0 1px 0 rgba(255, 255, 255, 0.4), inset 0 0 8px rgba(255, 255, 255, 0.12); +} +/* ---- Tables ---- */ +.table, +table.cbi-section-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; +} +.table thead tr th, +table.cbi-section-table thead tr th, +.table .tr.table-titles th, +table.cbi-section-table .tr.table-titles th, +.table .tr.cbi-section-table-titles th, +table.cbi-section-table .tr.cbi-section-table-titles th, +.table thead tr .th, +table.cbi-section-table thead tr .th, +.table .tr.table-titles .th, +table.cbi-section-table .tr.table-titles .th, +.table .tr.cbi-section-table-titles .th, +table.cbi-section-table .tr.cbi-section-table-titles .th { + padding: var(--space-sm) var(--space-lg); + text-align: left; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + color: var(--color-text-secondary); + border-bottom: 1px solid var(--color-separator); + white-space: nowrap; +} +.table tbody tr, +table.cbi-section-table tbody tr, +.table .tr, +table.cbi-section-table .tr { + /* Action button cells — right-aligned, equal-width buttons */ +} +.table tbody tr td, +table.cbi-section-table tbody tr td, +.table .tr td, +table.cbi-section-table .tr td, +.table tbody tr .td, +table.cbi-section-table tbody tr .td, +.table .tr .td, +table.cbi-section-table .tr .td { + padding: var(--space-md) var(--space-lg); + border-bottom: 0.5px solid var(--color-separator); + font-size: var(--font-size-sm); + vertical-align: middle; +} +.table tbody tr:last-child td, +table.cbi-section-table tbody tr:last-child td, +.table .tr:last-child td, +table.cbi-section-table .tr:last-child td, +.table tbody tr:last-child .td, +table.cbi-section-table tbody tr:last-child .td, +.table .tr:last-child .td, +table.cbi-section-table .tr:last-child .td { + border-bottom: none; +} +.table tbody tr td:last-child:has(.cbi-button), +table.cbi-section-table tbody tr td:last-child:has(.cbi-button), +.table .tr td:last-child:has(.cbi-button), +table.cbi-section-table .tr td:last-child:has(.cbi-button), +.table tbody tr .td:last-child:has(.cbi-button), +table.cbi-section-table tbody tr .td:last-child:has(.cbi-button), +.table .tr .td:last-child:has(.cbi-button), +table.cbi-section-table .tr .td:last-child:has(.cbi-button) { + text-align: right; + white-space: nowrap; +} +.table tbody tr td:last-child:has(.cbi-button) .cbi-button, +table.cbi-section-table tbody tr td:last-child:has(.cbi-button) .cbi-button, +.table .tr td:last-child:has(.cbi-button) .cbi-button, +table.cbi-section-table .tr td:last-child:has(.cbi-button) .cbi-button, +.table tbody tr .td:last-child:has(.cbi-button) .cbi-button, +table.cbi-section-table tbody tr .td:last-child:has(.cbi-button) .cbi-button, +.table .tr .td:last-child:has(.cbi-button) .cbi-button, +table.cbi-section-table .tr .td:last-child:has(.cbi-button) .cbi-button { + min-width: 6rem; + width: 6rem; + text-align: center; +} +.table tbody tr:hover td, +table.cbi-section-table tbody tr:hover td, +.table .tr:hover td, +table.cbi-section-table .tr:hover td, +.table tbody tr:hover .td, +table.cbi-section-table tbody tr:hover .td, +.table .tr:hover .td, +table.cbi-section-table .tr:hover .td { + background: rgba(0, 0, 0, 0.02); +} +/* ---- CBI Dynamic List (IP addresses, DNS servers, etc.) ---- */ +.cbi-dynlist { + line-height: 1.3; + flex-direction: column; + min-height: 30px; + cursor: text; + gap: 10px; +} +.cbi-dynlist > .item { + display: inline-flex; + flex-wrap: nowrap; + position: relative; + max-width: 25rem; + pointer-events: none; + color: var(--color-text-secondary); + outline: 0; + /* Remove button (×) */ +} +.cbi-dynlist > .item::after { + content: "\00D7"; + pointer-events: auto; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem !important; + height: 2.5rem; + margin: 0; + font-weight: normal; + font-size: 1rem; + line-height: 1.5rem; + color: var(--color-danger); + border: 0.5px solid rgba(255, 59, 48, 0.25); + border-left: none; + border-radius: 0 var(--radius-sm) var(--radius-sm) 0; + outline: 0; + background: rgba(255, 59, 48, 0.08); + background-image: none; + box-shadow: none; + box-sizing: border-box; + transition: background 0.2s; +} +.cbi-dynlist > .item::after:hover { + background: rgba(255, 59, 48, 0.18); +} +.cbi-dynlist > .item > span { + display: block; + padding: 0.5rem 0.75rem; + box-sizing: border-box; + min-width: 15.5rem; + width: 15.5rem; + height: 2.5rem; + overflow: hidden; + text-overflow: ellipsis; + user-select: text; + white-space: nowrap; + word-break: break-word; + line-height: 1.5em; + color: var(--color-text-primary); + border: 0.5px solid var(--color-border); + border-radius: var(--radius-sm) 0 0 var(--radius-sm); + border-right: none; + outline: 0; + background: var(--color-bg-elevated); + box-shadow: none; +} +.cbi-dynlist > .add-item { + display: inline-flex; + align-items: center; + width: 100%; + min-width: 15.5rem; + flex-wrap: nowrap; +} +.cbi-dynlist > .add-item input { + display: block; + padding: 0.5rem 0.75rem; + box-sizing: border-box; + min-width: 15.5rem; + width: 15.5rem; + white-space: nowrap; + font-size: var(--font-size-sm); + line-height: 1.5rem; + color: var(--color-text-primary); + border: 0.5px solid var(--color-border); + border-radius: var(--radius-sm) 0 0 var(--radius-sm); + border-right-width: 0; + outline: 0; + background: var(--color-bg-elevated); + box-shadow: none; +} +.cbi-dynlist > .add-item .cbi-button { + display: flex; + width: auto !important; + padding-left: 0.8rem; + padding-right: 0.8rem; + margin-left: 0; + align-items: center; + justify-content: center; + font-size: 0.875rem; + line-height: 1.5rem; + outline: 0; + box-shadow: none; + border-radius: 0 var(--radius-sm) var(--radius-sm) 0; +} +.cbi-dynlist > .add-item .cbi-button-add { + width: 2.5rem !important; + padding: 0.5rem 0 !important; + font-weight: normal; + font-size: 1.2rem; + color: #fff; + background: var(--color-primary); + border: 1px solid var(--color-primary); +} +/* ---- CBI Dropdown ---- */ +.cbi-dropdown { + position: relative; + display: inline-flex; + align-items: center; + min-width: 150px; + cursor: pointer; + /* Opened dropdown list */ + /* Hidden input container */ + /* When dropdown is used as a button (.btn / .cbi-button) */ + /* Arrow color inherits from button text */ +} +.cbi-dropdown > ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-wrap: wrap; + align-items: center; +} +.cbi-dropdown > ul > li { + display: none; + padding: var(--space-sm) var(--space-md); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-height: auto; + line-height: 1.4; + /* Hide placeholder
  • only when LuCI hasn't marked it as the + active display item. LuCI sets [display] on whichever
  • should + be shown — either the selected value or the placeholder itself + (e.g. Network Interfaces > Device dropdown with "unspecified"). + Without the :not([display]) guard the placeholder is hidden and + the dropdown appears empty except for the arrow + ([#6](https://github.com/rchen14b/luci-theme-glass/issues/6)). */ +} +.cbi-dropdown > ul > li .hide-close { + display: none; +} +.cbi-dropdown > ul > li[display] { + display: flex; + align-items: center; +} +.cbi-dropdown > ul > li[placeholder]:not([display]) { + display: none; +} +.cbi-dropdown > ul > li[placeholder][display] { + color: var(--color-text-tertiary); + font-style: italic; +} +.cbi-dropdown > .more, +.cbi-dropdown > .open { + flex-shrink: 0; + padding: 0 var(--space-xs); + cursor: pointer; + color: var(--color-text-secondary); + font-size: var(--font-size-sm); +} +.cbi-dropdown > .more { + display: none; +} +.cbi-dropdown[open] { + position: relative; + z-index: var(--z-modal); +} +.cbi-dropdown[open] > ul.dropdown { + /* Frosted glass dropdown list — matches sidebar/modal aesthetic. + Don't set top/left/bottom — LuCI's ui.js computes them inline based + on available viewport space. LuCI's inline `max-height` can clamp + to ~65px when the trigger is near the viewport edge, so override + with !important (matches argon). + ([#6](https://github.com/rchen14b/luci-theme-glass/issues/6)) */ + position: absolute; + z-index: var(--z-modal); + width: auto; + min-width: 100%; + max-width: none; + max-height: 250px !important; + overflow-y: auto; + display: block; + border-radius: var(--radius-md); + padding: var(--space-xs); + color: var(--color-text); + background: var(--glass-bg-strong, rgba(255, 255, 255, 0.85)); + backdrop-filter: blur(30px) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(30px) saturate(var(--glass-saturate)); + border: 0.5px solid var(--glass-border); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18), 0 2px 8px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.45); +} +.cbi-dropdown[open] > ul.dropdown > li { + display: flex; + align-items: center; + padding: var(--space-sm) var(--space-md); + border-radius: var(--radius-sm); + cursor: pointer; + color: var(--color-text); + transition: background var(--transition-fast), color var(--transition-fast); + white-space: nowrap; + line-height: 1.4; +} +.cbi-dropdown[open] > ul.dropdown > li:hover, +.cbi-dropdown[open] > ul.dropdown > li.focus { + background: rgba(255, 255, 255, 0.55); +} +.cbi-dropdown[open] > ul.dropdown > li[selected] { + color: var(--color-primary); + font-weight: var(--font-weight-medium); + background: rgba(var(--color-primary-rgb), 0.1); +} +.cbi-dropdown[open] > ul.dropdown > li .hide-open { + display: none; +} +.cbi-dropdown[open] > ul.dropdown > li .hide-close { + display: initial; +} +.cbi-dropdown[open] > ul.preview { + display: flex; +} +.cbi-dropdown > div { + display: none; +} +.cbi-dropdown.btn, +.cbi-dropdown.cbi-button { + padding: 0; +} +.cbi-dropdown.btn > ul > li[display], +.cbi-dropdown.cbi-button > ul > li[display] { + padding: var(--space-sm) var(--space-lg); +} +.cbi-dropdown.cbi-button-apply > .open, +.cbi-dropdown.cbi-button-positive > .open, +.cbi-dropdown.cbi-button-save > .open { + color: var(--color-primary); +} +/* ---- Tabs ---- */ +.cbi-tabmenu { + display: flex; + flex-wrap: wrap; + gap: var(--space-xs); + list-style: none; + margin: 0 0 var(--space-xl); + padding: var(--space-xs); + padding-left: var(--space-xs); + border-radius: var(--radius-md); + /* Glass tab bar — match header/sub-nav pattern */ + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.12) 25%, rgba(240, 248, 255, 0.13) 50%, rgba(255, 255, 255, 0.11) 75%, rgba(230, 240, 255, 0.12) 100%); + background-color: transparent; + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + border: 0.5px solid rgba(255, 255, 255, 0.12); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08), 0 1px 4px rgba(0, 0, 0, 0.04), inset 0 0 0.5px rgba(255, 255, 255, 0.08), inset 0 0 12px rgba(255, 255, 255, 0.1); +} +.cbi-tabmenu li a { + display: block; + padding: var(--space-sm) var(--space-lg); + border-radius: var(--radius-sm); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + transition: all var(--transition-fast); + text-decoration: none; +} +.cbi-tabmenu li a:hover { + color: var(--color-text); + background: rgba(0, 0, 0, 0.04); +} +.cbi-tabmenu li.cbi-tab a { + background: rgba(255, 255, 255, 0.45); + color: var(--color-text); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), inset 0 1px 1px rgba(255, 255, 255, 0.6), inset 0 0 6px rgba(255, 255, 255, 0.15); +} +/* Tab content panels — hide inactive tabs. + Use [data-tab] selector (tab panels have data-tab + data-tab-title). + Individual form fields may have data-tab-title only — don't hide those. + IMPORTANT: Do NOT use !important here — LuCI's dependency system needs + to evaluate fields in all tabs during page load to set proper visibility. */ +[data-tab][data-tab-title]:not([data-tab-active="true"]) { + display: none; +} +/* ---- Alerts / Messages ---- + Only the container is tinted — body text stays at `--color-text` so + plugins like Attended Sysupgrade, which dump labels + version numbers + inside the alert, remain readable. Tint applies to heading (`h4`) and + the left border accent instead + ([#7?]). */ +.alert-message, +.cbi-section-error { + padding: var(--space-md) var(--space-lg); + border-radius: var(--radius-md); + font-size: var(--font-size-sm); + margin-bottom: var(--space-lg); + color: var(--color-text); + background: rgba(90, 200, 250, 0.08); + border: 0.5px solid rgba(90, 200, 250, 0.2); + border-left: 3px solid var(--color-info); + backdrop-filter: blur(10px) saturate(180%); + -webkit-backdrop-filter: blur(10px) saturate(180%); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04), inset 0 1px 0 rgba(255, 255, 255, 0.3), inset 0 0 6px rgba(255, 255, 255, 0.08); +} +.alert-message > h4, +.cbi-section-error > h4, +.alert-message > .title, +.cbi-section-error > .title, +.alert-message > strong:first-child, +.cbi-section-error > strong:first-child { + color: var(--color-info); + margin-bottom: var(--space-xs); +} +.alert-message.notice, +.alert-message.info { + background: rgba(90, 200, 250, 0.1); + border: 0.5px solid rgba(90, 200, 250, 0.3); + border-left: 3px solid var(--color-info); +} +.alert-message.warning { + background: rgba(255, 149, 0, 0.1); + border: 0.5px solid rgba(255, 149, 0, 0.3); + border-left: 3px solid var(--color-warning); +} +.alert-message.warning > h4, +.alert-message.warning > .title, +.alert-message.warning > strong:first-child { + color: var(--color-warning); +} +.alert-message.error, +.cbi-section-error { + background: rgba(255, 59, 48, 0.1); + border: 0.5px solid rgba(255, 59, 48, 0.3); + border-left: 3px solid var(--color-danger); +} +.alert-message.error > h4, +.cbi-section-error > h4, +.alert-message.error > .title, +.cbi-section-error > .title, +.alert-message.error > strong:first-child, +.cbi-section-error > strong:first-child { + color: var(--color-danger); +} +.alert-message.success { + background: rgba(52, 199, 89, 0.1); + border: 0.5px solid rgba(52, 199, 89, 0.3); + border-left: 3px solid var(--color-success); +} +.alert-message.success > h4, +.alert-message.success > .title, +.alert-message.success > strong:first-child { + color: var(--color-success); +} +/* ---- Progress bars ---- */ +.cbi-progressbar { + position: relative; + height: 22px; + background: rgba(0, 0, 0, 0.06); + border-radius: var(--radius-full); + overflow: hidden; + border: 0.5px solid rgba(0, 0, 0, 0.04); +} +.cbi-progressbar > div { + height: 100%; + border-radius: var(--radius-full); + transition: width var(--transition-normal); + background: linear-gradient(135deg, rgba(var(--color-primary-rgb), 0.75) 0%, rgba(var(--color-primary-rgb), 0.6) 50%, rgba(var(--color-primary-rgb), 0.7) 100%); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3), inset 0 0 6px rgba(255, 255, 255, 0.1); +} +.cbi-progressbar::after { + content: attr(title); + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + color: var(--color-text); + text-shadow: 0 0 4px rgba(255, 255, 255, 0.6); +} +/* ---- Checkbox & Radio ---- */ +input[type="checkbox"], +input[type="radio"] { + accent-color: var(--color-primary); +} +/* ---- Spinning indicator ---- */ +.spinning { + position: relative; + padding-left: 32px !important; +} +.spinning::before { + position: absolute; + top: 0; + bottom: 0; + left: 0.2em; + width: 32px; + content: ""; + background: url(/luci-static/resources/icons/loading.svg) no-repeat center; + background-size: 16px; +} +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} +/* ---- Modal / Dialog ---- */ +#modal_overlay { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.35); + z-index: var(--z-modal); + /* Wide modals — CBI forms, sysupgrade, wireless edit */ +} +#modal_overlay > .modal { + background: var(--glass-bg-strong); + backdrop-filter: blur(30px) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(30px) saturate(var(--glass-saturate)); + border: 0.5px solid var(--glass-border); + box-shadow: var(--glass-shadow-lg); + border-radius: var(--radius-xl); + padding: var(--space-2xl); + max-width: 600px; + min-width: 270px; + width: calc(100% - var(--space-2xl) * 2); + max-height: calc(100vh - 4em); + overflow-y: auto; + z-index: calc(var(--z-modal) + 1); + /* Flatten any .cbi-section nested directly inside a modal — the modal + itself already has glass background + padding, so the inner section + creates a visible box-in-box with extra padding + ([#6](https://github.com/rchen14b/luci-theme-glass/issues/6)). */ + /* Let open dropdowns render beyond the modal's scroll clip. */ +} +#modal_overlay > .modal > * { + max-width: 100%; +} +#modal_overlay > .modal > pre, +#modal_overlay > .modal > textarea { + white-space: pre-wrap; + overflow: auto; + width: 100%; +} +#modal_overlay > .modal > .cbi-section, +#modal_overlay > .modal > form > .cbi-section, +#modal_overlay > .modal > .cbi-map > .cbi-section { + padding: 0; + margin-bottom: 0; + background: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; + border: none; + box-shadow: none; + border-radius: 0; +} +#modal_overlay > .modal:has(.cbi-dropdown[open]) { + overflow: visible; +} +#modal_overlay > .modal.cbi-modal, +#modal_overlay > .modal.uci-dialog { + max-width: 900px; +} +/* LuCI adds modal-overlay-active to , not to #modal_overlay */ +body.modal-overlay-active #modal_overlay { + display: flex; + align-items: center; + justify-content: center; +} +/* ── UCI Changes Dialog (Config / Changes) ── */ +.uci-dialog .cbi-section { + padding: 0; + margin: 0; + background: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; + border: none; + box-shadow: none; + border-radius: 0; +} +.uci-change-legend { + padding: 10px 0; + display: flex; + flex-wrap: wrap; + gap: 4px 16px; +} +.uci-change-legend-label { + display: inline-flex; + align-items: center; + font-size: var(--font-size-sm); +} +.uci-change-legend-label > ins, +.uci-change-legend-label > del, +.uci-change-legend-label > var { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 2px; + margin-right: 6px; + padding: 0; + text-decoration: none; + font-style: normal; + overflow: hidden; + /* hide text content inside the color swatch */ + text-indent: -9999px; +} +.uci-change-legend-label > ins { + background-color: rgba(52, 199, 89, 0.25); + border: 1px solid rgba(52, 199, 89, 0.5); +} +.uci-change-legend-label > del { + background-color: rgba(255, 59, 48, 0.25); + border: 1px solid rgba(255, 59, 48, 0.5); +} +.uci-change-legend-label > var { + background-color: rgba(90, 200, 250, 0.2); + border: 1px solid rgba(90, 200, 250, 0.4); +} +.uci-change-list { + font-family: inherit; + width: 100%; + display: flex; + flex-direction: column; + overflow: auto; + /* Nested ins/del inside var (inline changed values) */ +} +.uci-change-list ins, +.uci-change-list del, +.uci-change-list var { + display: block; + padding: 6px 10px; + margin-bottom: 4px; + border-radius: var(--radius-sm); + text-decoration: none; + font-style: normal; + font-size: var(--font-size-sm); + white-space: pre-wrap; + word-break: break-all; +} +.uci-change-list ins { + background-color: rgba(52, 199, 89, 0.1); + border: 0.5px solid rgba(52, 199, 89, 0.3); + color: var(--color-success); +} +.uci-change-list del { + background-color: rgba(255, 59, 48, 0.1); + border: 0.5px solid rgba(255, 59, 48, 0.3); + color: var(--color-danger); +} +.uci-change-list var { + background-color: rgba(90, 200, 250, 0.08); + border: 0.5px solid rgba(90, 200, 250, 0.2); + color: var(--color-info); +} +.uci-change-list var ins, +.uci-change-list var del { + display: inline; + padding: 0; + margin: 0; + border: none; + border-radius: 0; + background: none; +} +.uci-change-list var ins { + color: var(--color-success); +} +.uci-change-list var del { + color: var(--color-danger); +} +/* Button row inside uci-dialog */ +.uci-change-list + .right, +.uci-dialog .right { + margin-top: 16px; + display: flex; + gap: 8px; + justify-content: flex-end; +} +/* ========================================================================== + Login Page (sysauth) — luci-theme-glass + ========================================================================== */ +.login-wrap { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + padding: var(--space-xl); +} +.login-card { + /* True frosted glass — translucent, not white box */ + background-image: linear-gradient(160deg, rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0.2) 25%, rgba(245, 250, 255, 0.22) 50%, rgba(255, 255, 255, 0.18) 75%, rgba(240, 248, 255, 0.21) 100%); + background-color: rgba(255, 255, 255, 0.55); + backdrop-filter: blur(30px) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(30px) saturate(var(--glass-saturate)); + border: 0.5px solid rgba(255, 255, 255, 0.35); + box-shadow: 0 8px 40px rgba(0, 0, 0, 0.1), 0 2px 8px rgba(0, 0, 0, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.5), inset 0 0 16px rgba(255, 255, 255, 0.15); + border-radius: var(--radius-xl); + padding: var(--space-3xl) var(--space-2xl); + width: 100%; + max-width: 380px; + position: relative; + text-align: center; +} +.login-logo { + width: 64px; + height: 64px; + margin: 0 auto var(--space-lg); +} +.login-title { + font-size: var(--font-size-xl); + font-weight: var(--font-weight-bold); + margin-bottom: var(--space-xs); + color: var(--color-text); +} +.login-subtitle { + font-size: var(--font-size-sm); + color: var(--color-text-secondary); + margin-bottom: var(--space-sm); +} +.login-version { + font-size: var(--font-size-sm); + color: var(--color-text-tertiary); + margin-bottom: var(--space-2xl); +} +.login-form { + text-align: left; +} +.login-form .login-field { + margin-bottom: var(--space-lg); +} +.login-form .login-field label { + display: block; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + margin-bottom: var(--space-xs); +} +.login-form .login-field input { + width: 100%; + max-width: none; + padding: var(--space-md) var(--space-lg); + background: rgba(255, 255, 255, 0.3); + border: 0.5px solid rgba(255, 255, 255, 0.35); + border-radius: var(--radius-md); + font-size: var(--font-size-base); + color: var(--color-text); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), inset 0 0 4px rgba(255, 255, 255, 0.08); + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); +} +.login-form .login-field input:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb), 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.25); +} +.login-form .login-field input::placeholder { + color: var(--color-text-tertiary); +} +.login-btn { + width: 100%; + padding: var(--space-md) var(--space-lg); + /* Glass button — glass-ui .glass-bg pattern */ + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.12) 25%, rgba(240, 248, 255, 0.13) 50%, rgba(255, 255, 255, 0.11) 75%, rgba(230, 240, 255, 0.12) 100%); + background-color: transparent; + backdrop-filter: blur(var(--glass-blur)) saturate(180%); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(180%); + border: 0.5px solid rgba(255, 255, 255, 0.12); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08), 0 1px 4px rgba(0, 0, 0, 0.04), inset 0 0 0.5px rgba(255, 255, 255, 0.08), inset 0 0 12px rgba(255, 255, 255, 0.1); + color: var(--color-text); + border-radius: var(--radius-md); + font-size: var(--font-size-base); + font-weight: var(--font-weight-semibold); + cursor: pointer; + transition: all var(--transition-fast); +} +.login-btn:hover { + opacity: 0.9; +} +.login-btn:active { + transform: scale(0.98); + opacity: 0.8; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), inset 0 1px 2px rgba(0, 0, 0, 0.1); +} +.login-error { + background: rgba(255, 59, 48, 0.1); + border: 1px solid rgba(255, 59, 48, 0.3); + color: var(--color-danger); + padding: var(--space-md) var(--space-lg); + border-radius: var(--radius-md); + font-size: var(--font-size-sm); + margin-bottom: var(--space-lg); + text-align: center; +} +/* ========================================================================== + Responsive — Media queries for tablet & mobile + ========================================================================== */ +/* Tablet: sidebar overlay */ +@media (max-width: 1024px) { + #sidebar { + transform: translateX(calc(-100% - var(--sidebar-gap))); + } + #sidebar.open { + transform: translateX(0); + } + #header { + left: 0; + } + #header-sub { + left: 0; + max-width: calc(100vw - var(--sidebar-gap)); + } + #content-wrap { + margin-left: 0; + } + .glass-footer { + margin-left: 0; + } + .menu-toggle { + display: block; + } + .sidebar-overlay.active { + display: block; + } +} +/* Hide status indicators on small screens */ +@media (max-width: 768px) { + #status-indicators { + display: none !important; + } +} +/* Mobile: tighter spacing */ +@media (max-width: 640px) { + :root { + --content-padding: 16px; + --sidebar-width: 280px; + } + .cbi-value { + flex-direction: column; + } + .cbi-value-title { + flex: none; + padding-right: 0; + padding-bottom: var(--space-xs); + } + .cbi-page-actions { + flex-direction: column; + } + .cbi-page-actions .cbi-button { + width: 100%; + } + .login-card { + padding: var(--space-2xl) var(--space-xl); + } + .cbi-tabmenu { + overflow-x: auto; + flex-wrap: nowrap; + } + .cbi-tabmenu li a { + white-space: nowrap; + } + table.cbi-section-table, + .table { + display: block; + overflow-x: auto; + } +} +/* ========================================================================== + Page Fixes — LuCI-specific overrides and quirks + ========================================================================== */ +/* LuCI wraps everything in #maincontent */ +#maincontent { + padding: 0; +} +/* ---- Interface boxes (general) ---- */ +.ifacebox { + background: var(--glass-bg); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + border: 0.5px solid var(--glass-border); + box-shadow: var(--glass-shadow); + display: inline-flex; + flex-direction: column; + border-radius: var(--radius-md); + padding: 0; + margin-bottom: var(--space-md); + line-height: 1.25; + overflow: visible; + min-width: 100px; +} +.ifacebox-head { + font-weight: var(--font-weight-semibold); + padding: var(--space-sm) var(--space-lg); + margin: 0; + border-bottom: 0.5px solid var(--color-separator); + /* Zone-colored header — tinted glass instead of opaque */ +} +.ifacebox-head[style*="background-color"] { + background-color: rgba(var(--zone-color-rgb), 0.5) !important; + border-radius: var(--radius-md) var(--radius-md) 0 0; + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + border-bottom: 0.5px solid rgba(var(--zone-color-rgb), 0.2); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2); +} +.ifacebox-head.active { + background: linear-gradient(135deg, rgba(var(--color-primary-rgb), 0.75) 0%, rgba(var(--color-primary-rgb), 0.6) 50%, rgba(var(--color-primary-rgb), 0.7) 100%); + color: var(--color-text-inverse); + padding: var(--space-sm) var(--space-lg); + border-bottom: none; + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), inset 0 0 6px rgba(255, 255, 255, 0.08); +} +.ifacebox-body { + line-height: 1.5; + font-size: var(--font-size-sm); + padding: var(--space-sm) var(--space-lg) var(--space-lg); + /* L.itemlist output — key-value pairs */ + /* Device badge row */ +} +.ifacebox-body > span { + display: block; + padding: var(--space-xs) 0; +} +.ifacebox-body > span > .nowrap { + display: inline-block; + margin-right: var(--space-md); + margin-bottom: 2px; +} +.ifacebox-body > span > .nowrap > strong { + color: var(--color-text-secondary); + font-weight: var(--font-weight-medium); +} +.ifacebox-body > div { + margin-top: var(--space-sm); +} +/* ---- Port status grid (29_ports.js) ---- */ +div[style*="display:grid;grid-template-columns:repeat"] { + gap: var(--space-md) !important; + margin-bottom: var(--space-lg) !important; +} +div[style*="display:grid;grid-template-columns:repeat"] > .ifacebox { + margin: 0 !important; + padding: 0 !important; + text-align: center; + border-radius: var(--radius-md) !important; + /* Glass-ui card pattern */ + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.12) 25%, rgba(240, 248, 255, 0.13) 50%, rgba(255, 255, 255, 0.11) 75%, rgba(230, 240, 255, 0.12) 100%) !important; + background-color: transparent !important; + backdrop-filter: blur(10px) saturate(180%) !important; + -webkit-backdrop-filter: blur(10px) saturate(180%) !important; + border: 0.5px solid rgba(255, 255, 255, 0.12) !important; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08), 0 1px 4px rgba(0, 0, 0, 0.04), 0 0 0 0.5px rgba(255, 255, 255, 0.08) inset, 0 0 12px rgba(255, 255, 255, 0.1) inset !important; + /* Zone color bar */ +} +div[style*="display:grid;grid-template-columns:repeat"] > .ifacebox > .ifacebox-head { + padding: var(--space-xs) var(--space-sm); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + margin-bottom: 0; +} +div[style*="display:grid;grid-template-columns:repeat"] > .ifacebox > .ifacebox-body { + padding: var(--space-xs) var(--space-sm); + font-size: var(--font-size-sm); + line-height: 1.3; +} +div[style*="display:grid;grid-template-columns:repeat"] > .ifacebox > .ifacebox-body img { + width: 28px; + height: 28px; + margin: 2px auto; +} +div[style*="display:grid;grid-template-columns:repeat"] > .ifacebox > .ifacebox-head.cbi-tooltip-container { + padding: 0; + border-bottom: none; +} +div[style*="display:grid;grid-template-columns:repeat"] > .ifacebox > .ifacebox-head.cbi-tooltip-container .zonebadge { + border-radius: 0; + padding: 0; +} +/* ---- Network status table (30_network.js) ---- */ +.network-status-table { + display: flex; + flex-wrap: wrap; + gap: var(--space-md); +} +.network-status-table .ifacebox { + flex: 1 1 300px; + margin: 0; +} +.network-status-table .ifacebox-body { + display: flex; + flex-direction: column; + gap: var(--space-xs); +} +.network-status-table .ifacebox-body > div { + display: flex; + flex-wrap: wrap; + gap: var(--space-sm); +} +.network-status-table .ifacebox-body .ifacebadge { + flex: 1 1 auto; + min-width: 200px; +} +.network-status-table .ifacebox-body .ifacebadge > span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +/* ---- Interface badges ---- */ +.ifacebadge { + display: inline-flex; + align-items: center; + gap: var(--space-xs); + padding: 2px var(--space-sm); + margin-right: 0.4em; + min-width: max-content; + background: rgba(255, 255, 255, 0.4); + backdrop-filter: blur(8px) saturate(180%); + -webkit-backdrop-filter: blur(8px) saturate(180%); + border: 0.5px solid rgba(255, 255, 255, 0.2); + border-radius: var(--radius-sm); + font-size: var(--font-size-sm); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3), inset 0 0 4px rgba(255, 255, 255, 0.08); +} +.ifacebadge img { + width: 16px; + height: 16px; +} +/* Zone badges */ +.zonebadge { + display: inline-block; + padding: 2px var(--space-sm); + border-radius: var(--radius-sm); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + border: 0.5px solid rgba(255, 255, 255, 0.15); + backdrop-filter: blur(6px) saturate(160%); + -webkit-backdrop-filter: blur(6px) saturate(160%); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), inset 0 0 4px rgba(255, 255, 255, 0.06); +} +.zonebadge .ifacebadge { + margin: 1px 2px; + padding: 2px 4px; +} +/* ---- Tooltips ---- */ +.cbi-tooltip-container { + position: relative; +} +.cbi-tooltip { + display: none; + position: absolute; + z-index: var(--z-toast); + left: 0; + bottom: 100%; + margin-bottom: var(--space-xs); + background: var(--glass-bg); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + border: 0.5px solid var(--glass-border); + box-shadow: var(--glass-shadow); + border-radius: var(--radius-sm); + padding: var(--space-sm) var(--space-md); + font-size: var(--font-size-sm); + max-width: 300px; + white-space: normal; + line-height: 1.5; +} +.cbi-tooltip.left { + left: auto; + right: 0; +} +.cbi-tooltip-container:hover > .cbi-tooltip { + display: block; +} +/* System log */ +#syslog { + font-family: var(--font-mono); + font-size: var(--font-size-sm); + background: rgba(0, 0, 0, 0.04); + border-radius: var(--radius-md); + padding: var(--space-lg); + overflow-y: hidden; + width: 100%; + white-space: pre-wrap; + word-break: break-all; +} +/* Terminal / command output */ +pre { + background: rgba(0, 0, 0, 0.04); + border-radius: var(--radius-md); + padding: var(--space-lg); + overflow-x: auto; + font-family: var(--font-mono); + font-size: var(--font-size-sm); +} +/* Flatten pre inside modals — avoid box-in-box */ +.modal pre { + background: none; + border: none; + box-shadow: none; + border-radius: 0; + padding: var(--space-sm) 0; +} +/* Native progress element (package manager, etc.) */ +progress { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 8px; + border: none; + border-radius: var(--radius-full); + overflow: hidden; + background: rgba(0, 0, 0, 0.06); +} +progress::-webkit-progress-bar { + background: rgba(0, 0, 0, 0.06); + border-radius: var(--radius-full); +} +progress::-webkit-progress-value { + background: linear-gradient(135deg, rgba(var(--color-primary-rgb), 0.75) 0%, rgba(var(--color-primary-rgb), 0.6) 50%, rgba(var(--color-primary-rgb), 0.7) 100%); + border-radius: var(--radius-full); +} +progress::-moz-progress-bar { + background: linear-gradient(135deg, rgba(var(--color-primary-rgb), 0.75) 0%, rgba(var(--color-primary-rgb), 0.6) 50%, rgba(var(--color-primary-rgb), 0.7) 100%); + border-radius: var(--radius-full); +} +/* ACL/UBUS error page styling */ +#error-message { + text-align: center; + padding: var(--space-3xl); + color: var(--color-text-secondary); +} +/* LuCI diagnostics ping/traceroute output */ +#diag-rc-output { + border-radius: var(--radius-md); + padding: var(--space-lg); + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.12) 25%, rgba(240, 248, 255, 0.13) 50%, rgba(255, 255, 255, 0.11) 75%, rgba(230, 240, 255, 0.12) 100%); + background-color: transparent; + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + border: 0.5px solid rgba(255, 255, 255, 0.12); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.03), inset 0 1px 0 rgba(255, 255, 255, 0.3), inset 0 0 6px rgba(255, 255, 255, 0.08); +} +/* File upload */ +.cbi-filebrowser { + border-radius: var(--radius-md); + padding: var(--space-lg); + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, rgba(255, 255, 255, 0.12) 25%, rgba(240, 248, 255, 0.13) 50%, rgba(255, 255, 255, 0.11) 75%, rgba(230, 240, 255, 0.12) 100%); + background-color: transparent; + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + border: 0.5px solid rgba(255, 255, 255, 0.12); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.03), inset 0 1px 0 rgba(255, 255, 255, 0.3), inset 0 0 6px rgba(255, 255, 255, 0.08); +} +/* Apply config spinner */ +#cbi_apply_overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.25); + z-index: var(--z-overlay); + display: flex; + align-items: center; + justify-content: center; +} +#cbi_apply_overlay .spinning { + width: 40px; + height: 40px; + padding-left: 0 !important; + border: 3px solid var(--color-bg-secondary); + border-top-color: var(--color-primary); + border-radius: 50%; + animation: spin 1s linear infinite; +} +#cbi_apply_overlay .spinning::before { + display: none; +} +/* ---- Utility classes ---- */ +.nowrap { + white-space: nowrap; +} +.left { + text-align: left; +} +.center { + text-align: center; +} +.right { + text-align: right; +} +@media (max-width: 640px) { + .hide-xs { + display: none !important; + } +} +/* ---- Standalone section headings (DHCP, WiFi assoclist) ---- */ +#maincontent h3, +#view h3 { + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semibold); + margin: var(--space-xl) 0 var(--space-md); +} +/* ---- Table sort indicators ---- */ +.th[data-sort-direction] { + cursor: pointer; + user-select: none; +} +.th[data-sort-direction]::after { + content: ''; + display: inline-block; + width: 0; + height: 0; + margin-left: var(--space-xs); + vertical-align: middle; + border-left: 4px solid transparent; + border-right: 4px solid transparent; +} +.th[data-sort-direction][data-sort-direction="asc"]::after { + border-bottom: 5px solid var(--color-text-secondary); +} +.th[data-sort-direction][data-sort-direction="desc"]::after { + border-top: 5px solid var(--color-text-secondary); +} +.th[data-sortable-row] { + cursor: pointer; +} +/* ---- Table placeholder (empty state) ---- */ +.tr.placeholder .td { + text-align: center; + padding: var(--space-2xl) var(--space-lg); + color: var(--color-text-tertiary); + font-style: italic; +} +/* ---- Software / Package Manager page ---- */ +[data-page="admin-system-opkg"], +[data-page="admin-system-package-manager"] { + /* Action buttons — glass pill style */ + /* Override opkg.js inline CSS for controls layout */ + /* All control sections — override inline flex: 1 1 33% */ + /* Disk space — full width row */ + /* Filter — share row, grow equally */ + /* Download / install — share row, grow equally */ + /* Actions — own row */ + /* Translation filter — own row */ + /* Labels — block above their control group */ + /* Control groups — input + button inline */ + /* Action buttons — inline row with gap */ +} +[data-page="admin-system-opkg"] div.btn, +[data-page="admin-system-package-manager"] div.btn { + display: inline-flex; + padding: var(--space-sm) var(--space-lg); +} +[data-page="admin-system-opkg"] .controls, +[data-page="admin-system-package-manager"] .controls { + display: flex !important; + flex-wrap: wrap !important; + justify-content: flex-start !important; + gap: var(--space-md) var(--space-lg) !important; + margin: 0 0 var(--space-lg) 0 !important; +} +[data-page="admin-system-opkg"] .controls > div, +[data-page="admin-system-package-manager"] .controls > div { + padding: 0 !important; + white-space: normal !important; + display: flex !important; + flex-wrap: wrap !important; + align-items: flex-end !important; +} +[data-page="admin-system-opkg"] .controls > div:first-child, +[data-page="admin-system-package-manager"] .controls > div:first-child { + flex: 1 1 100% !important; +} +[data-page="admin-system-opkg"] .controls > div:nth-child(2), +[data-page="admin-system-package-manager"] .controls > div:nth-child(2) { + flex: 1 1 200px !important; +} +[data-page="admin-system-opkg"] .controls > div:nth-child(3), +[data-page="admin-system-package-manager"] .controls > div:nth-child(3) { + flex: 1 1 280px !important; +} +[data-page="admin-system-opkg"] .controls > div:nth-child(4), +[data-page="admin-system-package-manager"] .controls > div:nth-child(4) { + flex: 1 1 100% !important; +} +[data-page="admin-system-opkg"] .controls > div:nth-child(5), +[data-page="admin-system-package-manager"] .controls > div:nth-child(5) { + flex: 1 1 100% !important; +} +[data-page="admin-system-opkg"] .controls > div > label, +[data-page="admin-system-package-manager"] .controls > div > label { + flex-basis: 100% !important; + min-width: 0 !important; + margin-bottom: var(--space-xs) !important; + font-weight: var(--font-weight-medium) !important; + font-size: var(--font-size-sm) !important; + color: var(--color-text-secondary) !important; +} +[data-page="admin-system-opkg"] .controls .control-group, +[data-page="admin-system-package-manager"] .controls .control-group { + display: flex !important; + gap: var(--space-sm) !important; + flex: 1 1 auto !important; + align-items: center !important; +} +[data-page="admin-system-opkg"] .controls .control-group input[type="text"], +[data-page="admin-system-package-manager"] .controls .control-group input[type="text"] { + flex: 1 1 auto !important; + min-width: 120px !important; + width: auto !important; +} +[data-page="admin-system-opkg"] .controls > div:nth-child(4) > .control-group, +[data-page="admin-system-package-manager"] .controls > div:nth-child(4) > .control-group { + gap: var(--space-sm) !important; +} diff --git a/luci-theme-glass/htdocs/luci-static/glass/css/dark.css b/luci-theme-glass/htdocs/luci-static/glass/css/dark.css new file mode 100644 index 000000000..fd004961f --- /dev/null +++ b/luci-theme-glass/htdocs/luci-static/glass/css/dark.css @@ -0,0 +1,339 @@ +/* ========================================================================== + Dark Mode — luci-theme-glass + Compiled separately and injected by header.ut when dark mode is active. + ========================================================================== */ +:root { + /* ---- Colors — Dark mode ---- */ + --color-primary: #0A84FF; + --color-primary-hover: #409cff; + --color-primary-active: #0066d6; + --color-primary-rgb: 10, 132, 255; + --color-success: #30D158; + --color-warning: #FFD60A; + --color-danger: #FF453A; + --color-info: #64D2FF; + --color-text: #f5f5f7; + --color-text-secondary: #98989d; + --color-text-tertiary: #636366; + --color-text-inverse: #1d1d1f; + --color-bg: #1c1c1e; + --color-bg-elevated: #2c2c2e; + --color-bg-secondary: #3a3a3c; + --color-border: rgba(255, 255, 255, 0.08); + --color-border-strong: rgba(255, 255, 255, 0.16); + --color-separator: rgba(84, 84, 88, 0.36); + /* ---- Dark glassmorphism ---- */ + --glass-blur: 25px; + --glass-saturate: 180%; + --glass-bg: rgba(38, 38, 42, 0.55); + --glass-bg-hover: rgba(52, 52, 56, 0.65); + --glass-bg-strong: rgba(38, 38, 42, 0.72); + --glass-border: rgba(255, 255, 255, 0.12); + --glass-shadow: 0 2px 16px rgba(0, 0, 0, 0.2), 0 0 1px rgba(0, 0, 0, 0.25); + --glass-shadow-lg: 0 8px 40px rgba(0, 0, 0, 0.3), 0 0 1px rgba(0, 0, 0, 0.15); + /* ---- Dark background ---- */ + --bg-gradient: linear-gradient(135deg, #2c2e3a 0%, #353848 50%, #2a2d38 100%); +} +/* ---- Dark-mode-specific overrides ---- */ +/* Inputs need darker backgrounds */ +.cbi-input-text, +.cbi-input-password, +input[type="text"], +input[type="password"], +input[type="number"], +textarea, +select { + background: rgba(44, 44, 46, 0.55); +} +/* Login card */ +.login-card { + background-image: linear-gradient(160deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%); + background-color: rgba(38, 38, 42, 0.55); + border-color: rgba(255, 255, 255, 0.14); + box-shadow: 0 8px 40px rgba(0, 0, 0, 0.25), 0 2px 8px rgba(0, 0, 0, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.1), inset 0 0 16px rgba(255, 255, 255, 0.06); +} +.login-form .login-field input { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.15); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), inset 0 0 4px rgba(255, 255, 255, 0.04); +} +.login-btn { + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.06) 25%, rgba(240, 248, 255, 0.07) 50%, rgba(255, 255, 255, 0.05) 75%, rgba(230, 240, 255, 0.06) 100%); + border-color: rgba(255, 255, 255, 0.15); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.15), inset 0 0 0.5px rgba(255, 255, 255, 0.12), inset 0 0 12px rgba(255, 255, 255, 0.15); + color: var(--color-text); +} +/* Buttons — glass-ui dark pattern */ +.btn, +.cbi-button { + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.06) 25%, rgba(240, 248, 255, 0.07) 50%, rgba(255, 255, 255, 0.05) 75%, rgba(230, 240, 255, 0.06) 100%); + background-color: transparent; + border-color: rgba(255, 255, 255, 0.15); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.15), inset 0 0 0.5px rgba(255, 255, 255, 0.12), inset 0 0 12px rgba(255, 255, 255, 0.15); +} +.btn:hover, +.cbi-button:hover { + opacity: 0.9; + background-color: transparent; +} +.btn:active, +.cbi-button:active { + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3), inset 0 1px 2px rgba(0, 0, 0, 0.2); +} +.cbi-button-negative, +.cbi-button-remove, +.cbi-button-reset { + background-image: linear-gradient(135deg, rgba(255, 69, 58, 0.12) 0%, rgba(255, 69, 58, 0.08) 50%, rgba(255, 69, 58, 0.1) 100%); + background-color: transparent; + color: var(--color-danger); + border-color: rgba(255, 69, 58, 0.25); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3), 0 1px 4px rgba(0, 0, 0, 0.15), inset 0 0 0.5px rgba(255, 255, 255, 0.08), inset 0 0 8px rgba(255, 255, 255, 0.06); +} +.cbi-button-negative:hover, +.cbi-button-remove:hover, +.cbi-button-reset:hover { + opacity: 0.9; + background-color: transparent; +} +.cbi-button-negative:active, +.cbi-button-remove:active, +.cbi-button-reset:active { + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3), inset 0 1px 2px rgba(0, 0, 0, 0.2); +} +/* Sidebar */ +#sidebar { + background-image: linear-gradient(160deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 20%, rgba(240, 248, 255, 0.08) 45%, rgba(255, 255, 255, 0.06) 70%, rgba(230, 240, 255, 0.07) 100%); + background-color: rgba(38, 38, 42, 0.55); + border-color: rgba(255, 255, 255, 0.14); + box-shadow: 0 8px 40px rgba(0, 0, 0, 0.25), 0 2px 8px rgba(0, 0, 0, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.1), inset 0 0 12px rgba(255, 255, 255, 0.06); +} +/* Header bar */ +#header { + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%); + border-color: rgba(255, 255, 255, 0.15); + box-shadow: 0 2px 16px rgba(0, 0, 0, 0.2), 0 1px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1), inset 0 0 8px rgba(255, 255, 255, 0.06); +} +/* Page actions bar */ +.cbi-page-actions { + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%); + border-color: rgba(255, 255, 255, 0.15); + box-shadow: 0 2px 16px rgba(0, 0, 0, 0.2), 0 1px 4px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1), inset 0 0 8px rgba(255, 255, 255, 0.06); +} +/* Nav hover & active */ +.nav-item:hover { + background: rgba(255, 255, 255, 0.06); +} +.nav-item.active { + background: rgba(255, 255, 255, 0.15); + color: var(--color-text); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3), inset 0 1px 2px rgba(255, 255, 255, 0.12), inset 0 0 8px rgba(255, 255, 255, 0.08); +} +/* Sub-item active — slider handles highlight */ +.nav-sub .nav-item.active { + background: transparent; + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; +} +/* Sliding selector — dark glass */ +.nav-sub .nav-slider { + background: rgba(255, 255, 255, 0.08); + border: 0.5px solid rgba(255, 255, 255, 0.12); + backdrop-filter: blur(12px) saturate(150%); + -webkit-backdrop-filter: blur(12px) saturate(150%); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2), inset 0 1px 1px rgba(255, 255, 255, 0.08), inset 0 0 4px rgba(255, 255, 255, 0.03); +} +/* Header tabs */ +.header-tab:hover { + background: rgba(255, 255, 255, 0.06); + color: var(--color-text); +} +.header-tab.active { + background: transparent; + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; +} +/* Tab slider — dark glass */ +.tab-slider { + background: rgba(255, 255, 255, 0.1); + border: 0.5px solid rgba(255, 255, 255, 0.12); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3), inset 0 1px 2px rgba(255, 255, 255, 0.1), inset 0 0 8px rgba(255, 255, 255, 0.06); +} +/* Sub-nav bar */ +#header-sub { + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%); + border-color: rgba(255, 255, 255, 0.15); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2), 0 1px 4px rgba(0, 0, 0, 0.1), 0 0 0 0.5px rgba(255, 255, 255, 0.12) inset, 0 0 12px rgba(255, 255, 255, 0.15) inset; +} +.sub-tab:hover { + background: rgba(255, 255, 255, 0.06); +} +.sub-tab.active { + background: transparent; + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; +} +/* Header glass pills */ +.header-title, +#status-indicators, +.header-actions { + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.04) 0%, rgba(255, 255, 255, 0.02) 50%, rgba(255, 255, 255, 0.03) 100%); + border-color: rgba(255, 255, 255, 0.1); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25), 0 1px 2px rgba(0, 0, 0, 0.12), inset 0 0.5px 0 rgba(255, 255, 255, 0.06); +} +/* Theme toggle */ +.theme-toggle:hover { + background: rgba(255, 255, 255, 0.08); +} +.theme-toggle-float { + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%); + border-color: rgba(255, 255, 255, 0.15); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18), 0 1px 2px rgba(0, 0, 0, 0.08), inset 0 0 0.5px rgba(255, 255, 255, 0.12), inset 0 1px 6px rgba(255, 255, 255, 0.12); +} +/* Tabs */ +.cbi-tabmenu { + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%); + border-color: rgba(255, 255, 255, 0.15); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2), 0 1px 4px rgba(0, 0, 0, 0.1), inset 0 0 0.5px rgba(255, 255, 255, 0.12), inset 0 0 12px rgba(255, 255, 255, 0.15); +} +.cbi-tabmenu li a:hover { + background: rgba(255, 255, 255, 0.06); +} +.cbi-tabmenu li.cbi-tab a { + background: rgba(255, 255, 255, 0.15); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3), inset 0 1px 2px rgba(255, 255, 255, 0.12), inset 0 0 8px rgba(255, 255, 255, 0.08); +} +/* Section fold button */ +.cbi-section.fade-in .cbi-title > div:last-child span:hover { + background: rgba(255, 255, 255, 0.08); +} +/* Table row hover */ +table tbody tr:hover td, +.table .tr:hover .td { + background: rgba(255, 255, 255, 0.03); +} +/* Pre / code */ +pre, +#syslog { + background: rgba(255, 255, 255, 0.04); +} +/* Alert colors adjusted for dark */ +.alert-message, +.cbi-section-error { + background: rgba(100, 210, 255, 0.08); + border-color: rgba(100, 210, 255, 0.18); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.08), inset 0 0 6px rgba(255, 255, 255, 0.04); +} +.alert-message.notice, +.alert-message.info { + background: rgba(100, 210, 255, 0.1); + border-color: rgba(100, 210, 255, 0.25); +} +.alert-message.warning { + background: rgba(255, 214, 10, 0.1); + border-color: rgba(255, 214, 10, 0.25); +} +.alert-message.error, +.cbi-section-error { + background: rgba(255, 69, 58, 0.1); + border-color: rgba(255, 69, 58, 0.25); +} +.alert-message.success { + background: rgba(48, 209, 88, 0.1); + border-color: rgba(48, 209, 88, 0.25); +} +/* Diagnostics & file browser in dark */ +#diag-rc-output, +.cbi-filebrowser { + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%); + border-color: rgba(255, 255, 255, 0.15); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18), 0 1px 2px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.08), inset 0 0 6px rgba(255, 255, 255, 0.04); +} +/* Ifacebox zone header in dark — slightly brighter tint */ +.ifacebox-head[style*="background-color"] { + background-color: rgba(var(--zone-color-rgb), 0.2) !important; + border-bottom-color: rgba(var(--zone-color-rgb), 0.25) !important; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08); +} +/* Zone badges in dark — tint zone color instead of opaque background */ +.zonebadge[style*="background-color"] { + background-color: rgba(var(--zone-color-rgb), 0.25) !important; + border-color: rgba(var(--zone-color-rgb), 0.3); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), inset 0 0 4px rgba(255, 255, 255, 0.03); +} +.zonebadge { + border-color: rgba(255, 255, 255, 0.08); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), inset 0 0 4px rgba(255, 255, 255, 0.03); +} +/* Ifacebox in dark */ +.ifacebadge { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), inset 0 0 4px rgba(255, 255, 255, 0.03); +} +/* Port grid boxes in dark — glass-ui dark card */ +div[style*="display:grid;grid-template-columns:repeat"] > .ifacebox { + background-image: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, rgba(255, 255, 255, 0.07) 25%, rgba(240, 248, 255, 0.08) 50%, rgba(255, 255, 255, 0.06) 75%, rgba(230, 240, 255, 0.07) 100%) !important; + border-color: rgba(255, 255, 255, 0.15) !important; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2), 0 1px 4px rgba(0, 0, 0, 0.1), 0 0 0 0.5px rgba(255, 255, 255, 0.12) inset, 0 0 12px rgba(255, 255, 255, 0.15) inset !important; +} +/* Network status table badges in dark */ +.network-status-table .ifacebox-body .ifacebadge { + background: rgba(255, 255, 255, 0.06); +} +/* Dropdown open items hover */ +.cbi-dropdown[open] > ul.dropdown > li:hover { + background: rgba(255, 255, 255, 0.06); +} +/* Progressbar */ +.cbi-progressbar { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.06); +} +.cbi-progressbar > div { + background: linear-gradient(135deg, rgba(var(--color-primary-rgb), 0.65) 0%, rgba(var(--color-primary-rgb), 0.5) 50%, rgba(var(--color-primary-rgb), 0.6) 100%); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), inset 0 0 6px rgba(255, 255, 255, 0.06); +} +.cbi-progressbar::after { + color: var(--color-text); + text-shadow: 0 0 4px rgba(0, 0, 0, 0.4); +} +/* Ifacebox-body itemlist labels */ +.ifacebox-body > span > .nowrap > strong { + color: var(--color-text-tertiary); +} +/* UCI Changes dialog in dark */ +.uci-change-legend-label > ins { + background-color: rgba(48, 209, 88, 0.3); + border-color: rgba(48, 209, 88, 0.5); +} +.uci-change-legend-label > del { + background-color: rgba(255, 69, 58, 0.3); + border-color: rgba(255, 69, 58, 0.5); +} +.uci-change-legend-label > var { + background-color: rgba(100, 210, 255, 0.25); + border-color: rgba(100, 210, 255, 0.4); +} +.uci-change-list ins { + background-color: rgba(48, 209, 88, 0.12); + border-color: rgba(48, 209, 88, 0.25); +} +.uci-change-list del { + background-color: rgba(255, 69, 58, 0.12); + border-color: rgba(255, 69, 58, 0.25); +} +.uci-change-list var { + background-color: rgba(100, 210, 255, 0.1); + border-color: rgba(100, 210, 255, 0.18); +} +/* Selection */ +::selection { + background: rgba(var(--color-primary-rgb), 0.35); +} diff --git a/luci-theme-glass/htdocs/luci-static/glass/img/glass-logo.svg b/luci-theme-glass/htdocs/luci-static/glass/img/glass-logo.svg new file mode 100644 index 000000000..e697e803e --- /dev/null +++ b/luci-theme-glass/htdocs/luci-static/glass/img/glass-logo.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/luci-theme-glass/htdocs/luci-static/resources/menu-glass.js b/luci-theme-glass/htdocs/luci-static/resources/menu-glass.js new file mode 100644 index 000000000..5890b04ee --- /dev/null +++ b/luci-theme-glass/htdocs/luci-static/resources/menu-glass.js @@ -0,0 +1,327 @@ +'use strict'; +'require baseclass'; +'require ui'; + +return baseclass.extend({ + __init__: function() { + ui.menu.load().then(L.bind(this.render, this)); + }, + + /* Icon SVG paths for top-level sidebar categories (stroke-based, 24x24 viewBox) */ + icons: { + 'status': '', + 'system': '', + 'network': '', + 'services': '', + 'nas': '', + 'vpn': '', + 'docker': '', + 'logout': '', + '_default': '' + }, + + getIcon: function(name) { + var pathData = this.icons[name] || this.icons['_default']; + var markup = '' + pathData + ''; + var doc = new DOMParser().parseFromString(markup, 'image/svg+xml'); + return document.importNode(doc.documentElement, true); + }, + + render: function(tree) { + var container = document.getElementById('sidebar-nav'); + if (!container) return; + + /* Clear existing menu elements to prevent duplicates on re-render (e.g. Save & Apply) */ + container.innerHTML = ''; + + var headerNav = document.getElementById('header-nav'); + if (headerNav) { + /* Remove only tabs and sliders, preserve header-title */ + var old = headerNav.querySelectorAll('.header-tab, .tab-slider'); + for (var k = 0; k < old.length; k++) old[k].remove(); + } + + var headerTabs = document.getElementById('header-tabs'); + if (headerTabs) headerTabs.innerHTML = ''; + + var topChildren = ui.menu.getChildren(tree); + + for (var i = 0; i < topChildren.length; i++) { + var topNode = topChildren[i]; + var isTopActive = L.env.requestpath.length + ? topNode.name == L.env.requestpath[0] + : i == 0; + + if (isTopActive) { + this.renderSidebarMenu(container, topNode, topNode.name); + } + } + + /* Render mode menu (top-level tabs) if more than one category */ + if (topChildren.length > 1) { + this.renderModeMenu(topChildren); + } + + /* Set header title from the deepest active menu node */ + var titleEl = document.getElementById('header-title'); + if (titleEl) { + var node = tree; + var title = ''; + for (var i = 0; i < L.env.dispatchpath.length && node; i++) { + node = node.children[L.env.dispatchpath[i]]; + if (node && node.title) + title = node.title; + } + titleEl.textContent = title ? _(title) : ''; + } + + /* Render tab menu for sub-sub pages */ + if (L.env.dispatchpath.length >= 3) { + var node = tree; + var url = ''; + for (var i = 0; i < 3 && node; i++) { + node = node.children[L.env.dispatchpath[i]]; + url = url + (url ? '/' : '') + L.env.dispatchpath[i]; + } + if (node) + this.renderTabMenu(node, url); + } + }, + + /* Shared: position a slider behind the active tab element */ + positionTabSlider: function(container, slider, activeEl, animate) { + if (!activeEl) return; + if (!animate) slider.style.transition = 'none'; + slider.style.left = activeEl.offsetLeft + 'px'; + slider.style.width = activeEl.offsetWidth + 'px'; + slider.style.opacity = '1'; + if (!animate) { + requestAnimationFrame(function() { + slider.style.transition = ''; + }); + } + }, + + /* Shared: add slider to a horizontal tab container */ + addTabSlider: function(container, navigateOnClick) { + var slider = E('div', { 'class': 'tab-slider' }); + container.insertBefore(slider, container.firstChild); + var self = this; + + /* Attach click handlers to all tabs */ + var tabs = container.querySelectorAll('.header-tab, .sub-tab'); + for (var i = 0; i < tabs.length; i++) { + (function(tab, sl) { + tab.addEventListener('click', function(ev) { + if (navigateOnClick) { + /* Slide first, then navigate */ + ev.preventDefault(); + var prev = container.querySelector('.header-tab.active, .sub-tab.active'); + if (prev) prev.classList.remove('active'); + tab.classList.add('active'); + self.positionTabSlider(container, sl, tab, true); + document.body.classList.add('page-leaving'); + var href = tab.href; + setTimeout(function() { + window.location.href = href; + }, 180); + } else { + /* In-page tab switch — just move slider */ + self.positionTabSlider(container, sl, tab, true); + } + }); + })(tabs[i], slider); + } + + /* Position on active tab */ + requestAnimationFrame(function() { + var active = container.querySelector('.header-tab.active, .sub-tab.active'); + self.positionTabSlider(container, slider, active, false); + }); + + return slider; + }, + + renderModeMenu: function(topChildren) { + var container = document.getElementById('header-nav'); + if (!container) return; + + for (var i = 0; i < topChildren.length; i++) { + var child = topChildren[i]; + var isActive = L.env.requestpath.length + ? child.name == L.env.requestpath[0] + : i == 0; + + var item = E('a', { + 'class': 'header-tab' + (isActive ? ' active' : ''), + 'href': L.url(child.name), + 'data-name': child.name + }, [ _(child.title) ]); + + container.appendChild(item); + } + + /* Add sliding selector */ + this.addTabSlider(container, true); + }, + + renderSidebarMenu: function(container, tree, url) { + var children = ui.menu.getChildren(tree); + if (children.length == 0) return; + + var group = E('div', { 'class': 'nav-group' }); + + for (var i = 0; i < children.length; i++) { + var child = children[i]; + var subChildren = ui.menu.getChildren(child); + var hasChildren = subChildren.length > 0; + + var isActive = L.env.dispatchpath[1] == child.name; + + /* For items with children, link to first child */ + var linkUrl = hasChildren + ? L.url(url, child.name, subChildren[0].name) + : L.url(url, child.name); + + var item = E('a', { + 'class': 'nav-item' + (isActive ? ' open' : ''), + 'href': linkUrl, + 'data-name': child.name + }, [ + E('span', { 'class': 'nav-label' }, [ _(child.title) ]) + ]); + + /* Add icon before the label */ + item.insertBefore(this.getIcon(child.name), item.firstChild); + + if (hasChildren) { + item.appendChild(E('svg', { + 'class': 'nav-arrow', + 'viewBox': '0 0 24 24', + 'fill': 'none', + 'stroke': 'currentColor', + 'stroke-width': '2' + }, [ + E('polyline', { 'points': '9 18 15 12 9 6' }) + ])); + + } + + group.appendChild(item); + + if (hasChildren) { + var subMenu = E('div', { + 'class': 'nav-sub' + (isActive ? ' open' : '') + }); + + /* Sliding highlight indicator */ + var slider = E('div', { 'class': 'nav-slider' }); + subMenu.appendChild(slider); + + for (var j = 0; j < subChildren.length; j++) { + var sub = subChildren[j]; + var isSubActive = isActive && L.env.dispatchpath[2] == sub.name; + + var subItem = E('a', { + 'class': 'nav-item' + (isSubActive ? ' active' : ''), + 'href': L.url(url, child.name, sub.name) + }, [ + E('span', { 'class': 'nav-label' }, [ _(sub.title) ]) + ]); + + /* Slide highlight to clicked item before navigating */ + (function(el, sl) { + el.addEventListener('click', function(ev) { + ev.preventDefault(); + var prev = el.parentNode.querySelector('.nav-item.active'); + if (prev) prev.classList.remove('active'); + el.classList.add('active'); + sl.style.top = el.offsetTop + 'px'; + sl.style.height = el.offsetHeight + 'px'; + sl.style.opacity = '1'; + document.body.classList.add('page-leaving'); + setTimeout(function() { + window.location.href = el.href; + }, 180); + }); + })(subItem, slider); + + subMenu.appendChild(subItem); + } + + /* Position slider on the active item after render */ + (function(sm, sl) { + requestAnimationFrame(function() { + var active = sm.querySelector('.nav-item.active'); + if (active) { + sl.style.transition = 'none'; + sl.style.top = active.offsetTop + 'px'; + sl.style.height = active.offsetHeight + 'px'; + sl.style.opacity = '1'; + /* Re-enable transition after initial position */ + requestAnimationFrame(function() { + sl.style.transition = ''; + }); + } + }); + })(subMenu, slider); + + group.appendChild(subMenu); + + /* Bind click handler to toggle submenu */ + (function(navItem, subEl) { + navItem.addEventListener('click', function(ev) { + ev.preventDefault(); + var isOpen = subEl.classList.contains('open'); + if (isOpen) { + subEl.classList.remove('open'); + navItem.classList.remove('open'); + } else { + subEl.classList.add('open'); + navItem.classList.add('open'); + } + }); + })(item, subMenu); + } + } + + container.appendChild(group); + }, + + renderTabMenu: function(tree, url, level) { + var container = document.getElementById('header-sub'); + if (!container) return E([]); + + var children = ui.menu.getChildren(tree); + var activeNode = null; + + if (children.length == 0) + return E([]); + + /* Clear any CBI tabs that footer.ut added before menu loaded */ + container.innerHTML = ''; + container.classList.remove('active'); + document.body.classList.remove('has-sub-nav'); + + for (var i = 0; i < children.length; i++) { + var child = children[i]; + var isActive = L.env.dispatchpath[3 + (level || 0)] == child.name; + + container.appendChild(E('a', { + 'class': 'sub-tab' + (isActive ? ' active' : ''), + 'href': L.url(url, child.name) + }, [ _(child.title) ])); + + if (isActive) + activeNode = child; + } + + container.classList.add('active'); + document.body.classList.add('has-sub-nav'); + + /* Add sliding selector */ + this.addTabSlider(container, true); + + /* Do NOT recurse — deeper menu-tree levels stay in-content as CBI tabmenus */ + } +}); diff --git a/luci-theme-glass/htdocs/luci-static/resources/status-glass.js b/luci-theme-glass/htdocs/luci-static/resources/status-glass.js new file mode 100644 index 000000000..566c9878f --- /dev/null +++ b/luci-theme-glass/htdocs/luci-static/resources/status-glass.js @@ -0,0 +1,286 @@ +'use strict'; +'require baseclass'; +'require rpc'; +'require fs'; + +var callSystemInfo = rpc.declare({ + object: 'system', + method: 'info' +}); + +var callInterfaceDump = rpc.declare({ + object: 'network.interface', + method: 'dump', + expect: { 'interface': [] } +}); + +var callDeviceStatus = rpc.declare({ + object: 'network.device', + method: 'status', + params: ['name'] +}); + +return baseclass.extend({ + prevStats: null, + prevTime: null, + netDevice: null, + netLabel: null, + netChecked: false, + linkSpeed: null, + numCores: 1, + + __init__: function() { + var self = this; + /* Detect core count once so load average can be converted to %. */ + L.resolveDefault(fs.read('/proc/cpuinfo'), '').then(function(text) { + var m = text.match(/^processor\s*:/gm); + self.numCores = (m && m.length) || 1; + }); + this.setupIndicators(); + }, + + icons: { + cpu: '', + ram: '', + net: '', + uptime: '' + }, + + makeIcon: function(name) { + var markup = this.icons[name]; + if (!markup) return document.createTextNode(''); + var doc = new DOMParser().parseFromString(markup, 'image/svg+xml'); + return document.importNode(doc.documentElement, true); + }, + + createIndicator: function(name, title) { + var el = document.createElement('span'); + el.setAttribute('data-indicator', name); + el.title = title; + el.appendChild(this.makeIcon(name)); + var val = document.createElement('span'); + val.className = 'indicator-value'; + /* Restore last known value from session to avoid flash of "--" on page nav */ + var cached = null; + try { cached = sessionStorage.getItem('glass-status-' + name); } catch(e) {} + val.textContent = cached || '--'; + el.appendChild(val); + return el; + }, + + setLevel: function(el, level) { + el.setAttribute('data-level', level); + }, + + setupIndicators: function() { + this.container = document.getElementById('status-indicators'); + if (!this.container) return; + + this.cpuEl = this.createIndicator('cpu', 'CPU Load'); + this.ramEl = this.createIndicator('ram', 'Memory Usage'); + this.uptimeEl = this.createIndicator('uptime', 'Uptime'); + + this.container.appendChild(this.cpuEl); + this.container.appendChild(this.ramEl); + this.container.appendChild(this.uptimeEl); + + this.fetchAndUpdate(); + setInterval(L.bind(this.fetchAndUpdate, this), 5000); + }, + + fetchAndUpdate: function() { + var self = this; + + L.resolveDefault(callSystemInfo(), {}).then(function(info) { + self.updateSystem(info); + }); + + if (!this.netChecked) { + this.netChecked = true; + L.resolveDefault(callInterfaceDump(), []).then(function(ifaces) { + /* Find WAN device — prefer 'wan' over 'wan6' */ + var wanDev = null; + for (var i = 0; i < ifaces.length; i++) { + var iface = ifaces[i]; + if (iface.interface === 'wan') { + wanDev = iface.l3_device || iface.device || null; + break; + } + } + if (!wanDev) { + for (var i = 0; i < ifaces.length; i++) { + if (ifaces[i].interface === 'wan6') { + wanDev = ifaces[i].l3_device || ifaces[i].device || null; + break; + } + } + } + + self.resolvePhysicalDevice(wanDev || 'wan'); + }); + } else if (this.netDevice) { + this.pollNetwork(); + } + }, + + resolvePhysicalDevice: function(devName) { + var self = this; + L.resolveDefault(callDeviceStatus(devName), {}).then(function(dev) { + if (!dev) return; + var members = dev['bridge-members']; + if (dev.type === 'bridge' && members && members.length > 0) { + /* Bridge stats only count management traffic; + use first physical port for real forwarded stats */ + self.netDevice = members[0]; + } else if (dev.statistics) { + self.netDevice = devName; + } + if (!self.netDevice) return; + + /* Query physical port for link speed and DSA info */ + L.resolveDefault(callDeviceStatus(self.netDevice), {}).then(function(phys) { + if (phys && phys.speed) { + var m = String(phys.speed).match(/^(\d+)/); + if (m) self.linkSpeed = parseInt(m[1], 10); + } + /* DSA port with hardware offload: per-port counters miss + offloaded flows; use the conduit (switch master) instead */ + if (phys && phys.devtype === 'dsa' && phys['hw-tc-offload'] && phys.conduit) { + self.netLabel = self.netDevice; + self.netDevice = phys.conduit; + } + self.netEl = self.createIndicator('net', 'Throughput'); + self.container.insertBefore(self.netEl, self.uptimeEl); + self.pollNetwork(); + }); + }); + }, + + pollNetwork: function() { + var self = this; + L.resolveDefault(callDeviceStatus(this.netDevice), {}).then(function(dev) { + self.updateNetwork(dev); + }); + }, + + updateSystem: function(info) { + if (!info) return; + + if (info.load) { + var load1 = info.load[0] / 65536; + var load5 = (info.load[1] / 65536).toFixed(2); + var load15 = (info.load[2] / 65536).toFixed(2); + var pct = Math.min(load1 / this.numCores, 1) * 100; + var pctStr = pct.toFixed(0) + '%'; + + this.cpuEl.querySelector('.indicator-value').textContent = pctStr; + try { sessionStorage.setItem('glass-status-cpu', pctStr); } catch(e) {} + this.cpuEl.title = 'CPU: ' + pctStr + ' (load ' + load1.toFixed(2) + + ' / ' + load5 + ' / ' + load15 + ' on ' + this.numCores + + (this.numCores === 1 ? ' core)' : ' cores)'); + + var level = pct < 60 ? 'ok' : pct < 85 ? 'warn' : 'crit'; + this.setLevel(this.cpuEl, level); + } + + if (info.memory) { + var total = info.memory.total; + var avail = info.memory.available || info.memory.free; + var used = total - avail; + var pct = (used / total * 100).toFixed(0); + this.ramEl.querySelector('.indicator-value').textContent = pct + '%'; + try { sessionStorage.setItem('glass-status-ram', pct + '%'); } catch(e) {} + this.ramEl.title = 'RAM: ' + this.formatBytes(used) + ' / ' + this.formatBytes(total) + ' (' + pct + '% used)'; + + var level = pct < 60 ? 'ok' : pct < 85 ? 'warn' : 'crit'; + this.setLevel(this.ramEl, level); + } + + if (info.uptime) { + this.uptimeEl.querySelector('.indicator-value').textContent = this.formatUptime(info.uptime); + try { sessionStorage.setItem('glass-status-uptime', this.formatUptime(info.uptime)); } catch(e) {} + this.uptimeEl.title = 'Uptime: ' + this.formatUptimeFull(info.uptime); + } + }, + + updateNetwork: function(dev) { + if (!dev || !dev.statistics || !this.netEl) return; + + var now = Date.now() / 1000; + var rx = dev.statistics.rx_bytes || 0; + var tx = dev.statistics.tx_bytes || 0; + + if (this.prevStats && this.prevTime) { + var dt = now - this.prevTime; + if (dt > 0) { + var rxSpeed = (rx - this.prevStats.rx) / dt; + var txSpeed = (tx - this.prevStats.tx) / dt; + if (rxSpeed < 0) rxSpeed = 0; + if (txSpeed < 0) txSpeed = 0; + var netText = '\u2193' + this.formatSpeed(rxSpeed) + ' \u2191' + this.formatSpeed(txSpeed); + this.netEl.querySelector('.indicator-value').textContent = netText; + try { sessionStorage.setItem('glass-status-net', netText); } catch(e) {} + var tip = (this.netLabel || this.netDevice) + ': \u2193 ' + this.formatSpeedFull(rxSpeed) + ' / \u2191 ' + this.formatSpeedFull(txSpeed); + if (this.linkSpeed) + tip += ' (Link: ' + (this.linkSpeed >= 1000 ? (this.linkSpeed / 1000) + ' Gbps' : this.linkSpeed + ' Mbps') + ')'; + this.netEl.title = tip; + + var peak = Math.max(rxSpeed, txSpeed); + var level = peak < 1048576 ? 'ok' : peak < 52428800 ? 'active' : 'busy'; + this.setLevel(this.netEl, level); + } + } + + this.prevStats = { rx: rx, tx: tx }; + this.prevTime = now; + }, + + formatUptime: function(s) { + var d = Math.floor(s / 86400); + var h = Math.floor((s % 86400) / 3600); + var m = Math.floor((s % 3600) / 60); + if (d > 0) return d + 'd ' + h + 'h'; + return h + 'h ' + m + 'm'; + }, + + formatUptimeFull: function(s) { + var d = Math.floor(s / 86400); + var h = Math.floor((s % 86400) / 3600); + var m = Math.floor((s % 3600) / 60); + var parts = []; + if (d > 0) parts.push(d + (d === 1 ? ' day' : ' days')); + if (h > 0) parts.push(h + (h === 1 ? ' hour' : ' hours')); + if (m > 0) parts.push(m + (m === 1 ? ' min' : ' mins')); + return parts.join(', ') || '< 1 min'; + }, + + formatBytes: function(bytes) { + if (bytes >= 1073741824) + return (bytes / 1073741824).toFixed(1) + ' GB'; + if (bytes >= 1048576) + return (bytes / 1048576).toFixed(0) + ' MB'; + return (bytes / 1024).toFixed(0) + ' KB'; + }, + + formatSpeed: function(bytesPerSec) { + var bits = bytesPerSec * 8; + if (bits >= 1000000000) + return (bits / 1000000000).toFixed(1) + 'G'; + if (bits >= 1000000) + return (bits / 1000000).toFixed(1) + 'M'; + if (bits >= 1000) + return (bits / 1000).toFixed(1) + 'K'; + return Math.round(bits) + 'b'; + }, + + formatSpeedFull: function(bytesPerSec) { + var bits = bytesPerSec * 8; + if (bits >= 1000000000) + return (bits / 1000000000).toFixed(1) + ' Gbps'; + if (bits >= 1000000) + return (bits / 1000000).toFixed(1) + ' Mbps'; + if (bits >= 1000) + return (bits / 1000).toFixed(1) + ' kbps'; + return Math.round(bits) + ' bps'; + } +}); diff --git a/luci-theme-glass/htdocs/luci-static/resources/view/system/glass.js b/luci-theme-glass/htdocs/luci-static/resources/view/system/glass.js new file mode 100644 index 000000000..9912d35b3 --- /dev/null +++ b/luci-theme-glass/htdocs/luci-static/resources/view/system/glass.js @@ -0,0 +1,137 @@ +'use strict'; +'require view'; +'require form'; +'require uci'; +'require fs'; + +return view.extend({ + load: function () { + return Promise.all([ + uci.load('glass'), + L.resolveDefault(fs.list('/www/luci-static/glass/background'), []) + ]); + }, + + render: function (data) { + var bgFiles = (data[1] || []).filter(function(f) { + return f.name && !f.name.startsWith('.') && /\.(jpe?g|png|gif|webp|mp4|webm)$/i.test(f.name); + }); + var hasBg = bgFiles.length > 0; + var bgName = hasBg ? bgFiles[0].name : null; + + var m, s, o; + + m = new form.Map('glass', _('Glass Theme'), + _('Configure the appearance of the Glass theme. Changes take effect after saving and refreshing the page.')); + + /* ── General ── */ + s = m.section(form.NamedSection, 'global', 'global', _('General')); + s.anonymous = true; + + o = s.option(form.ListValue, 'mode', _('Theme mode'), + _('Controls the overall appearance. "Normal" follows the system preference.')); + o.value('normal', _('Auto (follow system)')); + o.value('light', _('Light')); + o.value('dark', _('Dark')); + o.default = 'normal'; + + o = s.option(form.Flag, 'status_bar', _('Header status bar'), + _('Show live system stats (CPU, RAM, network, uptime) in the header. Disable to reduce resource usage on low-end devices.')); + o.default = '1'; + o.rmempty = false; + + o = s.option(form.ListValue, 'font_size', _('Base font size'), + _('Scales all text in the theme. Larger values improve readability on high-DPI displays.')); + o.value('13', _('Small (13px)')); + o.value('14', _('Normal (14px)')); + o.value('16', _('Large (16px)')); + o.value('18', _('Extra Large (18px)')); + o.default = '14'; + + /* ── Colors ── */ + s = m.section(form.NamedSection, 'global', 'global', _('Accent Colors')); + s.anonymous = true; + + o = s.option(form.Value, 'primary', _('Primary color (light mode)'), + _('Accent color used for active elements, links, and buttons in light mode.')); + o.default = '#007AFF'; + o.placeholder = '#007AFF'; + o.datatype = 'string'; + o.renderWidget = function(section_id, option_index, cfgvalue) { + var el = form.Value.prototype.renderWidget.apply(this, arguments); + var input = el.querySelector('input'); + if (input) { + input.type = 'color'; + input.style.height = '2.5rem'; + input.style.cursor = 'pointer'; + } + return el; + }; + + o = s.option(form.Value, 'dark_primary', _('Primary color (dark mode)'), + _('Accent color used for active elements, links, and buttons in dark mode.')); + o.default = '#0A84FF'; + o.placeholder = '#0A84FF'; + o.datatype = 'string'; + o.renderWidget = function(section_id, option_index, cfgvalue) { + var el = form.Value.prototype.renderWidget.apply(this, arguments); + var input = el.querySelector('input'); + if (input) { + input.type = 'color'; + input.style.height = '2.5rem'; + input.style.cursor = 'pointer'; + } + return el; + }; + + /* ── Glass Effects (Light Mode) ── */ + s = m.section(form.NamedSection, 'global', 'global', _('Glass Effects — Light Mode')); + s.anonymous = true; + + o = s.option(form.Value, 'blur', _('Blur intensity (px)'), + _('Backdrop blur radius for glass panels. Higher values create a more frosted appearance.')); + o.default = '20'; + o.placeholder = '20'; + o.datatype = 'range(0,50)'; + + o = s.option(form.Value, 'transparency', _('Panel transparency'), + _('Background opacity of glass panels (0 = fully transparent, 1 = fully opaque).')); + o.default = '0.72'; + o.placeholder = '0.72'; + o.datatype = 'string'; + + /* ── Glass Effects (Dark Mode) ── */ + s = m.section(form.NamedSection, 'global', 'global', _('Glass Effects — Dark Mode')); + s.anonymous = true; + + o = s.option(form.Value, 'blur_dark', _('Blur intensity (px)'), + _('Backdrop blur radius for glass panels in dark mode.')); + o.default = '25'; + o.placeholder = '25'; + o.datatype = 'range(0,50)'; + + o = s.option(form.Value, 'transparency_dark', _('Panel transparency'), + _('Background opacity of glass panels in dark mode (0 = fully transparent, 1 = fully opaque).')); + o.default = '0.30'; + o.placeholder = '0.30'; + o.datatype = 'string'; + + /* ── Background ── */ + s = m.section(form.NamedSection, 'global', 'global', _('Background')); + s.anonymous = true; + + o = s.option(form.DummyValue, '_bg_info', _('Current background')); + o.rawhtml = true; + o.cfgvalue = function () { + if (hasBg) { + return '' + + _('Active') + ': ' + bgName + ''; + } + return '' + + _('No background set. Upload an image or video named "bg" (e.g. bg.jpg, bg.png, bg.mp4) to /www/luci-static/glass/background/ via SCP.') + + ''; + }; + + return m.render(); + } +}); diff --git a/luci-theme-glass/less/cascade.less b/luci-theme-glass/less/cascade.less new file mode 100644 index 000000000..667cb344e --- /dev/null +++ b/luci-theme-glass/less/cascade.less @@ -0,0 +1,13 @@ +/* ========================================================================== + luci-theme-glass — Master Stylesheet + Import order matters: tokens → reset → mixins → layout → components → fixes + ========================================================================== */ + +@import "variables"; +@import "normalize"; +@import "glass"; +@import "layout"; +@import "components"; +@import "sysauth"; +@import "responsive"; +@import "page-fix"; diff --git a/luci-theme-glass/less/components.less b/luci-theme-glass/less/components.less new file mode 100644 index 000000000..965447d85 --- /dev/null +++ b/luci-theme-glass/less/components.less @@ -0,0 +1,1167 @@ +/* ========================================================================== + Components — CBI forms, tables, buttons, inputs, dropdowns + ========================================================================== */ + +/* ---- CBI Sections ---- */ +.cbi-section { + .glass-effect(); + border-radius: var(--radius-lg); + padding: var(--space-xl); + margin-bottom: var(--space-xl); + position: relative; + overflow-x: auto; + + /* Lift section above siblings when a dropdown is open inside it */ + &:has(.cbi-dropdown[open]) { + z-index: var(--z-dropdown); + } + + /* Nested sections — flatten (no extra box/padding) */ + .cbi-section { + padding: 0; + margin-bottom: 0; + background: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; + border: none; + box-shadow: none; + border-radius: 0; + } +} + +/* Allow dropdowns to escape stacking contexts created by backdrop-filter. + Temporarily remove glass effects on the section and force overflow visible + on all ancestors up to the section. */ +.cbi-section:has(.cbi-dropdown[open]) { + z-index: var(--z-modal); + backdrop-filter: none; + -webkit-backdrop-filter: none; + overflow: visible; +} + +.cbi-section:has(.cbi-dropdown[open]) .table, +.cbi-section:has(.cbi-dropdown[open]) .tr, +.cbi-section:has(.cbi-dropdown[open]) .td, +.cbi-section:has(.cbi-dropdown[open]) .cbi-value, +.cbi-section:has(.cbi-dropdown[open]) .cbi-value-field { + overflow: visible !important; +} + +.cbi-section-descr { + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + margin-bottom: var(--space-lg); + padding-bottom: var(--space-lg); + border-bottom: 0.5px solid var(--color-separator); +} + +.cbi-section > h3, +.cbi-section-node > h3 { + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semibold); + margin-bottom: var(--space-lg); +} + +/* ---- Section fold/collapse ---- */ +.cbi-section.fade-in .cbi-title { + display: flex; + align-items: center; + + > h3 { + flex: 1; + margin-bottom: 0; + } + + /* Move toggle button to the right of the title */ + > div:last-child { + flex-shrink: 0; + margin-left: var(--space-md); + } + + > div:last-child span { + font-size: 0; + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + cursor: pointer; + border-radius: var(--radius-sm); + transition: background var(--transition-fast); + + &:hover { + background: rgba(0, 0, 0, 0.06); + } + + &::after { + content: ''; + display: block; + width: 7px; + height: 7px; + border-right: 1.5px solid var(--color-text-tertiary); + border-bottom: 1.5px solid var(--color-text-tertiary); + transform: rotate(-135deg); + transition: transform 0.3s ease; + margin-top: 3px; + } + + &[data-style="inactive"]::after { + transform: rotate(45deg); + margin-top: -2px; + } + } +} + +/* ---- CBI Map (page-level container) ---- */ +.cbi-map { + margin-bottom: var(--space-xl); +} + +/* Title shown in header bar; description becomes tooltip */ +.cbi-map-descr, +.cbi-map > h2, +#view > h2, +#maincontent > h2 { + display: none; +} + +/* Hide descriptions only when moved to header tooltip (data-moved set by footer.ut JS) */ +.cbi-map-descr[data-moved], +#view > h2 + [data-moved] { + display: none; +} + +/* ---- CBI Value rows ---- */ +.cbi-value { + display: flex; + align-items: flex-start; + padding: var(--space-md) 0; + border-bottom: 0.5px solid var(--color-separator); + + &:last-child { + border-bottom: none; + } + + /* Plugins that use form.DummyValue.extend({ renderWidget }) to emit a + full-width section or table (usteer Hearing map, DHCP leases, etc.) + render the section as the *only* child of .cbi-value — no title, no + value-field wrapper. As a flex item with default `flex: 0 1 auto` it + shrinks to content width, leaving a big empty right zone + ([#8](https://github.com/rchen14b/luci-theme-glass/issues/8)). Switch + the row to block so the section spans full width. */ + &:has(> .cbi-section), + &:has(> table) { + display: block; + } +} + +.cbi-value-title { + flex: 0 0 200px; + padding-right: var(--space-xl); + font-weight: var(--font-weight-medium); + padding-top: var(--space-sm); + color: var(--color-text); +} + +.cbi-value-description { + color: var(--color-text-tertiary); + font-size: var(--font-size-sm); + margin-top: var(--space-xs); + font-weight: var(--font-weight-normal); +} + +.cbi-value-field { + flex: 1; + min-width: 0; +} + +/* ---- Inputs ---- */ +.cbi-input-text, +.cbi-input-password, +input[type="text"], +input[type="password"], +input[type="number"], +textarea { + width: 100%; + max-width: 400px; + padding: var(--space-sm) var(--space-md); + background: rgba(255, 255, 255, 0.55); + border: 1px solid var(--color-border-strong); + border-radius: var(--radius-md); + font-size: var(--font-size-base); + color: var(--color-text); + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); + outline: none; + + &:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb), 0.15); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +textarea { + max-width: 100%; + min-height: 80px; + resize: vertical; +} + +select { + padding: var(--space-sm) var(--space-md); + background: rgba(255, 255, 255, 0.55); + border: 1px solid var(--color-border-strong); + border-radius: var(--radius-md); + font-size: var(--font-size-base); + color: var(--color-text); + outline: none; + cursor: pointer; + + &:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb), 0.15); + } +} + +/* ---- Buttons — glass-ui pattern ---- */ +.btn, +.cbi-button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: var(--space-sm) var(--space-lg); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + border-radius: var(--radius-md); + /* Glass button surface */ + background-image: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.15) 0%, + rgba(255, 255, 255, 0.12) 25%, + rgba(240, 248, 255, 0.13) 50%, + rgba(255, 255, 255, 0.11) 75%, + rgba(230, 240, 255, 0.12) 100% + ); + background-color: rgba(255, 255, 255, 0.55); + border: 0.5px solid rgba(255, 255, 255, 0.30); + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.06), + 0 1px 2px rgba(0, 0, 0, 0.04), + inset 0 1px 0 rgba(255, 255, 255, 0.40), + inset 0 0 6px rgba(255, 255, 255, 0.10); + backdrop-filter: blur(10px) saturate(180%); + -webkit-backdrop-filter: blur(10px) saturate(180%); + color: var(--color-text); + cursor: pointer; + transition: all var(--transition-fast); + text-decoration: none; + white-space: nowrap; + gap: var(--space-xs); + + &:hover { + opacity: 0.90; + box-shadow: + 0 4px 12px rgba(0, 0, 0, 0.08), + 0 1px 3px rgba(0, 0, 0, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.45), + inset 0 0 8px rgba(255, 255, 255, 0.12); + } + + &:active { + transform: scale(0.98); + opacity: 0.85; + box-shadow: + inset 0 2px 4px rgba(0, 0, 0, 0.15), + inset 0 1px 2px rgba(0, 0, 0, 0.10); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; + } +} + +.cbi-button-positive, +.cbi-button-apply, +.cbi-button-action, +.cbi-button-reload, +.cbi-button[value="Submit"], +.cbi-button[value="Upload"], +.cbi-button[value$="Apply"] { + /* Primary glass — blue-tinted (Save & Apply, actions) */ + background-image: linear-gradient( + 135deg, + rgba(0, 122, 255, 0.10) 0%, + rgba(0, 122, 255, 0.07) 25%, + rgba(10, 132, 255, 0.09) 50%, + rgba(0, 122, 255, 0.06) 75%, + rgba(0, 100, 220, 0.08) 100% + ); + background-color: rgba(255, 255, 255, 0.45); + color: var(--color-primary); + border: 0.5px solid rgba(0, 122, 255, 0.18); + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.06), + 0 1px 2px rgba(0, 0, 0, 0.04), + inset 0 1px 0 rgba(255, 255, 255, 0.35), + inset 0 0 6px rgba(255, 255, 255, 0.08); + + &:hover { + opacity: 0.90; + color: var(--color-primary); + box-shadow: + 0 4px 12px rgba(0, 122, 255, 0.12), + 0 1px 3px rgba(0, 0, 0, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.40), + inset 0 0 8px rgba(255, 255, 255, 0.10); + } + + &:active { + box-shadow: + inset 0 2px 4px rgba(0, 0, 0, 0.15), + inset 0 1px 2px rgba(0, 0, 0, 0.10); + } +} + +.cbi-button-save { + /* Secondary glass — green-tinted (Save) */ + background-image: linear-gradient( + 135deg, + rgba(52, 199, 89, 0.10) 0%, + rgba(52, 199, 89, 0.07) 25%, + rgba(48, 209, 88, 0.09) 50%, + rgba(52, 199, 89, 0.06) 75%, + rgba(40, 180, 75, 0.08) 100% + ); + background-color: rgba(255, 255, 255, 0.45); + color: rgb(36, 138, 61); + border: 0.5px solid rgba(52, 199, 89, 0.20); + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.06), + 0 1px 2px rgba(0, 0, 0, 0.04), + inset 0 1px 0 rgba(255, 255, 255, 0.35), + inset 0 0 6px rgba(255, 255, 255, 0.08); + + &:hover { + opacity: 0.90; + color: rgb(36, 138, 61); + box-shadow: + 0 4px 12px rgba(52, 199, 89, 0.12), + 0 1px 3px rgba(0, 0, 0, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.40), + inset 0 0 8px rgba(255, 255, 255, 0.10); + } + + &:active { + box-shadow: + inset 0 2px 4px rgba(0, 0, 0, 0.15), + inset 0 1px 2px rgba(0, 0, 0, 0.10); + } +} + +.cbi-button-negative, +.cbi-button-remove, +.cbi-button-reset { + /* Tinted danger glass */ + background-image: linear-gradient( + 135deg, + rgba(255, 59, 48, 0.10) 0%, + rgba(255, 59, 48, 0.07) 50%, + rgba(255, 59, 48, 0.09) 100% + ); + background-color: rgba(255, 255, 255, 0.45); + color: var(--color-danger); + border: 0.5px solid rgba(255, 59, 48, 0.18); + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.05), + 0 1px 2px rgba(0, 0, 0, 0.03), + inset 0 1px 0 rgba(255, 255, 255, 0.35), + inset 0 0 6px rgba(255, 255, 255, 0.08); + + &:hover { + background-color: rgba(255, 255, 255, 0.50); + box-shadow: + 0 4px 12px rgba(255, 59, 48, 0.12), + 0 1px 3px rgba(0, 0, 0, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.40), + inset 0 0 8px rgba(255, 255, 255, 0.10); + } + + &:active { + box-shadow: + inset 0 2px 4px rgba(0, 0, 0, 0.15), + inset 0 1px 2px rgba(0, 0, 0, 0.10); + } +} + +.cbi-button-neutral, +.cbi-button-link, +.cbi-button-edit, +.cbi-button-find, +.cbi-button-add, +.cbi-button-download, +.cbi-button-up, +.cbi-button-down { + background-color: rgba(255, 255, 255, 0.40); + color: var(--color-text); +} + +.cbi-page-actions { + border-radius: var(--radius-lg); + padding: var(--space-md) var(--space-xl); + margin-top: var(--space-xl); + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--space-sm); + + /* Match header nav bar glass style */ + background-image: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.15) 0%, + rgba(255, 255, 255, 0.12) 25%, + rgba(240, 248, 255, 0.13) 50%, + rgba(255, 255, 255, 0.11) 75%, + rgba(230, 240, 255, 0.12) 100% + ); + background-color: transparent; + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + border: 0.5px solid rgba(255, 255, 255, 0.35); + box-shadow: + 0 2px 16px rgba(0, 0, 0, 0.06), + 0 1px 4px rgba(0, 0, 0, 0.03), + inset 0 1px 0 rgba(255, 255, 255, 0.40), + inset 0 0 8px rgba(255, 255, 255, 0.12); +} + +/* ---- Tables ---- */ +.table, +table.cbi-section-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + + thead tr, + .tr.table-titles, + .tr.cbi-section-table-titles { + th, .th { + padding: var(--space-sm) var(--space-lg); + text-align: left; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + color: var(--color-text-secondary); + border-bottom: 1px solid var(--color-separator); + white-space: nowrap; + } + } + + tbody tr, + .tr { + td, .td { + padding: var(--space-md) var(--space-lg); + border-bottom: 0.5px solid var(--color-separator); + font-size: var(--font-size-sm); + vertical-align: middle; + } + + &:last-child td, + &:last-child .td { + border-bottom: none; + } + + /* Action button cells — right-aligned, equal-width buttons */ + td:last-child:has(.cbi-button), + .td:last-child:has(.cbi-button) { + text-align: right; + white-space: nowrap; + + .cbi-button { + min-width: 6rem; + width: 6rem; + text-align: center; + } + } + + &:hover td, + &:hover .td { + background: rgba(0, 0, 0, 0.02); + } + } +} + +/* ---- CBI Dynamic List (IP addresses, DNS servers, etc.) ---- */ +.cbi-dynlist { + line-height: 1.3; + flex-direction: column; + min-height: 30px; + cursor: text; + gap: 10px; + + > .item { + display: inline-flex; + flex-wrap: nowrap; + position: relative; + max-width: 25rem; + pointer-events: none; + color: var(--color-text-secondary); + outline: 0; + + /* Remove button (×) */ + &::after { + content: "\00D7"; + pointer-events: auto; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem !important; + height: 2.5rem; + margin: 0; + font-weight: normal; + font-size: 1rem; + line-height: 1.5rem; + color: var(--color-danger); + border: 0.5px solid rgba(255, 59, 48, 0.25); + border-left: none; + border-radius: 0 var(--radius-sm) var(--radius-sm) 0; + outline: 0; + background: rgba(255, 59, 48, 0.08); + background-image: none; + box-shadow: none; + box-sizing: border-box; + transition: background 0.2s; + } + + &::after:hover { + background: rgba(255, 59, 48, 0.18); + } + + > span { + display: block; + padding: 0.5rem 0.75rem; + box-sizing: border-box; + min-width: 15.5rem; + width: 15.5rem; + height: 2.5rem; + overflow: hidden; + text-overflow: ellipsis; + user-select: text; + white-space: nowrap; + word-break: break-word; + line-height: 1.5em; + color: var(--color-text-primary); + border: 0.5px solid var(--color-border); + border-radius: var(--radius-sm) 0 0 var(--radius-sm); + border-right: none; + outline: 0; + background: var(--color-bg-elevated); + box-shadow: none; + } + } + + > .add-item { + display: inline-flex; + align-items: center; + width: 100%; + min-width: 15.5rem; + flex-wrap: nowrap; + + input { + display: block; + padding: 0.5rem 0.75rem; + box-sizing: border-box; + min-width: 15.5rem; + width: 15.5rem; + white-space: nowrap; + font-size: var(--font-size-sm); + line-height: 1.5rem; + color: var(--color-text-primary); + border: 0.5px solid var(--color-border); + border-radius: var(--radius-sm) 0 0 var(--radius-sm); + border-right-width: 0; + outline: 0; + background: var(--color-bg-elevated); + box-shadow: none; + } + + .cbi-button { + display: flex; + width: auto !important; + padding-left: 0.8rem; + padding-right: 0.8rem; + margin-left: 0; + align-items: center; + justify-content: center; + font-size: 0.875rem; + line-height: 1.5rem; + outline: 0; + box-shadow: none; + border-radius: 0 var(--radius-sm) var(--radius-sm) 0; + } + + .cbi-button-add { + width: 2.5rem !important; + padding: 0.5rem 0 !important; + font-weight: normal; + font-size: 1.2rem; + color: #fff; + background: var(--color-primary); + border: 1px solid var(--color-primary); + } + } +} + +/* ---- CBI Dropdown ---- */ +.cbi-dropdown { + position: relative; + display: inline-flex; + align-items: center; + min-width: 150px; + cursor: pointer; + + > ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-wrap: wrap; + align-items: center; + + > li { + display: none; + padding: var(--space-sm) var(--space-md); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-height: auto; + line-height: 1.4; + + .hide-close { + display: none; + } + + &[display] { + display: flex; + align-items: center; + } + + /* Hide placeholder
  • only when LuCI hasn't marked it as the + active display item. LuCI sets [display] on whichever
  • should + be shown — either the selected value or the placeholder itself + (e.g. Network Interfaces > Device dropdown with "unspecified"). + Without the :not([display]) guard the placeholder is hidden and + the dropdown appears empty except for the arrow + ([#6](https://github.com/rchen14b/luci-theme-glass/issues/6)). */ + &[placeholder]:not([display]) { + display: none; + } + + &[placeholder][display] { + color: var(--color-text-tertiary); + font-style: italic; + } + } + } + + > .more, + > .open { + flex-shrink: 0; + padding: 0 var(--space-xs); + cursor: pointer; + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + } + + > .more { + display: none; + } + + /* Opened dropdown list */ + &[open] { + position: relative; + z-index: var(--z-modal); + } + + &[open] > ul.dropdown { + /* Frosted glass dropdown list — matches sidebar/modal aesthetic. + Don't set top/left/bottom — LuCI's ui.js computes them inline based + on available viewport space. LuCI's inline `max-height` can clamp + to ~65px when the trigger is near the viewport edge, so override + with !important (matches argon). + ([#6](https://github.com/rchen14b/luci-theme-glass/issues/6)) */ + position: absolute; + z-index: var(--z-modal); + width: auto; + min-width: 100%; + max-width: none; + max-height: 250px !important; + overflow-y: auto; + display: block; + border-radius: var(--radius-md); + padding: var(--space-xs); + color: var(--color-text); + background: var(--glass-bg-strong, rgba(255, 255, 255, 0.85)); + backdrop-filter: blur(30px) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(30px) saturate(var(--glass-saturate)); + border: 0.5px solid var(--glass-border); + box-shadow: + 0 12px 40px rgba(0, 0, 0, 0.18), + 0 2px 8px rgba(0, 0, 0, 0.08), + inset 0 1px 0 rgba(255, 255, 255, 0.45); + + > li { + display: flex; + align-items: center; + padding: var(--space-sm) var(--space-md); + border-radius: var(--radius-sm); + cursor: pointer; + color: var(--color-text); + transition: background var(--transition-fast), color var(--transition-fast); + white-space: nowrap; + line-height: 1.4; + + &:hover, + &.focus { + background: rgba(255, 255, 255, 0.55); + } + + &[selected] { + color: var(--color-primary); + font-weight: var(--font-weight-medium); + background: rgba(var(--color-primary-rgb), 0.10); + } + + .hide-open { + display: none; + } + + .hide-close { + display: initial; + } + } + } + + &[open] > ul.preview { + display: flex; + } + + /* Hidden input container */ + > div { + display: none; + } + + /* When dropdown is used as a button (.btn / .cbi-button) */ + &.btn, + &.cbi-button { + padding: 0; + + > ul > li[display] { + padding: var(--space-sm) var(--space-lg); + } + } + + /* Arrow color inherits from button text */ + &.cbi-button-apply > .open, + &.cbi-button-positive > .open, + &.cbi-button-save > .open { + color: var(--color-primary); + } +} + +/* ---- Tabs ---- */ +.cbi-tabmenu { + display: flex; + flex-wrap: wrap; + gap: var(--space-xs); + list-style: none; + margin: 0 0 var(--space-xl); + padding: var(--space-xs); + padding-left: var(--space-xs); + border-radius: var(--radius-md); + /* Glass tab bar — match header/sub-nav pattern */ + background-image: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.15) 0%, + rgba(255, 255, 255, 0.12) 25%, + rgba(240, 248, 255, 0.13) 50%, + rgba(255, 255, 255, 0.11) 75%, + rgba(230, 240, 255, 0.12) 100% + ); + background-color: transparent; + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + border: 0.5px solid rgba(255, 255, 255, 0.12); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.08), + 0 1px 4px rgba(0, 0, 0, 0.04), + inset 0 0 0.5px rgba(255, 255, 255, 0.08), + inset 0 0 12px rgba(255, 255, 255, 0.10); + + li { + a { + display: block; + padding: var(--space-sm) var(--space-lg); + border-radius: var(--radius-sm); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + transition: all var(--transition-fast); + text-decoration: none; + + &:hover { + color: var(--color-text); + background: rgba(0, 0, 0, 0.04); + } + } + + &.cbi-tab a { + background: rgba(255, 255, 255, 0.45); + color: var(--color-text); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.08), + inset 0 1px 1px rgba(255, 255, 255, 0.60), + inset 0 0 6px rgba(255, 255, 255, 0.15); + } + } +} + +/* Tab content panels — hide inactive tabs. + Use [data-tab] selector (tab panels have data-tab + data-tab-title). + Individual form fields may have data-tab-title only — don't hide those. + IMPORTANT: Do NOT use !important here — LuCI's dependency system needs + to evaluate fields in all tabs during page load to set proper visibility. */ +[data-tab][data-tab-title]:not([data-tab-active="true"]) { + display: none; +} + +/* ---- Alerts / Messages ---- + Only the container is tinted — body text stays at `--color-text` so + plugins like Attended Sysupgrade, which dump labels + version numbers + inside the alert, remain readable. Tint applies to heading (`h4`) and + the left border accent instead + ([#7?]). */ +.alert-message, +.cbi-section-error { + padding: var(--space-md) var(--space-lg); + border-radius: var(--radius-md); + font-size: var(--font-size-sm); + margin-bottom: var(--space-lg); + color: var(--color-text); + background: rgba(90, 200, 250, 0.08); + border: 0.5px solid rgba(90, 200, 250, 0.20); + border-left: 3px solid var(--color-info); + backdrop-filter: blur(10px) saturate(180%); + -webkit-backdrop-filter: blur(10px) saturate(180%); + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.04), + inset 0 1px 0 rgba(255, 255, 255, 0.30), + inset 0 0 6px rgba(255, 255, 255, 0.08); + + > h4, > .title, > strong:first-child { + color: var(--color-info); + margin-bottom: var(--space-xs); + } +} + +.alert-message.notice, +.alert-message.info { + background: rgba(90, 200, 250, 0.10); + border: 0.5px solid rgba(90, 200, 250, 0.30); + border-left: 3px solid var(--color-info); +} + +.alert-message.warning { + background: rgba(255, 149, 0, 0.10); + border: 0.5px solid rgba(255, 149, 0, 0.30); + border-left: 3px solid var(--color-warning); + + > h4, > .title, > strong:first-child { + color: var(--color-warning); + } +} + +.alert-message.error, +.cbi-section-error { + background: rgba(255, 59, 48, 0.10); + border: 0.5px solid rgba(255, 59, 48, 0.30); + border-left: 3px solid var(--color-danger); + + > h4, > .title, > strong:first-child { + color: var(--color-danger); + } +} + +.alert-message.success { + background: rgba(52, 199, 89, 0.10); + border: 0.5px solid rgba(52, 199, 89, 0.30); + border-left: 3px solid var(--color-success); + + > h4, > .title, > strong:first-child { + color: var(--color-success); + } +} + +/* ---- Progress bars ---- */ +.cbi-progressbar { + position: relative; + height: 22px; + background: rgba(0, 0, 0, 0.06); + border-radius: var(--radius-full); + overflow: hidden; + border: 0.5px solid rgba(0, 0, 0, 0.04); + + > div { + height: 100%; + border-radius: var(--radius-full); + transition: width var(--transition-normal); + background: linear-gradient( + 135deg, + rgba(var(--color-primary-rgb), 0.75) 0%, + rgba(var(--color-primary-rgb), 0.60) 50%, + rgba(var(--color-primary-rgb), 0.70) 100% + ); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.30), + inset 0 0 6px rgba(255, 255, 255, 0.10); + } + + &::after { + content: attr(title); + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + color: var(--color-text); + text-shadow: 0 0 4px rgba(255, 255, 255, 0.60); + } +} + +/* ---- Checkbox & Radio ---- */ +input[type="checkbox"], +input[type="radio"] { + accent-color: var(--color-primary); +} + +/* ---- Spinning indicator ---- */ +.spinning { + position: relative; + padding-left: 32px !important; +} + +.spinning::before { + position: absolute; + top: 0; + bottom: 0; + left: 0.2em; + width: 32px; + content: ""; + background: url(/luci-static/resources/icons/loading.svg) no-repeat center; + background-size: 16px; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* ---- Modal / Dialog ---- */ +#modal_overlay { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.35); + z-index: var(--z-modal); + + > .modal { + .glass-frosted(); + border-radius: var(--radius-xl); + padding: var(--space-2xl); + max-width: 600px; + min-width: 270px; + width: calc(100% - var(--space-2xl) * 2); + max-height: calc(100vh - 4em); + overflow-y: auto; + z-index: calc(var(--z-modal) + 1); + + > * { + max-width: 100%; + } + + > pre, > textarea { + white-space: pre-wrap; + overflow: auto; + width: 100%; + } + + /* Flatten any .cbi-section nested directly inside a modal — the modal + itself already has glass background + padding, so the inner section + creates a visible box-in-box with extra padding + ([#6](https://github.com/rchen14b/luci-theme-glass/issues/6)). */ + > .cbi-section, + > form > .cbi-section, + > .cbi-map > .cbi-section { + padding: 0; + margin-bottom: 0; + background: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; + border: none; + box-shadow: none; + border-radius: 0; + } + + /* Let open dropdowns render beyond the modal's scroll clip. */ + &:has(.cbi-dropdown[open]) { + overflow: visible; + } + } + + /* Wide modals — CBI forms, sysupgrade, wireless edit */ + > .modal.cbi-modal, + > .modal.uci-dialog { + max-width: 900px; + } +} + +/* LuCI adds modal-overlay-active to , not to #modal_overlay */ +body.modal-overlay-active #modal_overlay { + display: flex; + align-items: center; + justify-content: center; +} + +/* ── UCI Changes Dialog (Config / Changes) ── */ +.uci-dialog .cbi-section { + padding: 0; + margin: 0; + background: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; + border: none; + box-shadow: none; + border-radius: 0; +} +.uci-change-legend { + padding: 10px 0; + display: flex; + flex-wrap: wrap; + gap: 4px 16px; +} + +.uci-change-legend-label { + display: inline-flex; + align-items: center; + font-size: var(--font-size-sm); + + > ins, > del, > var { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 2px; + margin-right: 6px; + padding: 0; + text-decoration: none; + font-style: normal; + overflow: hidden; + /* hide text content inside the color swatch */ + text-indent: -9999px; + } + + > ins { + background-color: rgba(52, 199, 89, 0.25); + border: 1px solid rgba(52, 199, 89, 0.5); + } + + > del { + background-color: rgba(255, 59, 48, 0.25); + border: 1px solid rgba(255, 59, 48, 0.5); + } + + > var { + background-color: rgba(90, 200, 250, 0.20); + border: 1px solid rgba(90, 200, 250, 0.4); + } +} + +.uci-change-list { + font-family: inherit; + width: 100%; + display: flex; + flex-direction: column; + overflow: auto; + + ins, del, var { + display: block; + padding: 6px 10px; + margin-bottom: 4px; + border-radius: var(--radius-sm); + text-decoration: none; + font-style: normal; + font-size: var(--font-size-sm); + white-space: pre-wrap; + word-break: break-all; + } + + ins { + background-color: rgba(52, 199, 89, 0.10); + border: 0.5px solid rgba(52, 199, 89, 0.30); + color: var(--color-success); + } + + del { + background-color: rgba(255, 59, 48, 0.10); + border: 0.5px solid rgba(255, 59, 48, 0.30); + color: var(--color-danger); + } + + var { + background-color: rgba(90, 200, 250, 0.08); + border: 0.5px solid rgba(90, 200, 250, 0.20); + color: var(--color-info); + } + + /* Nested ins/del inside var (inline changed values) */ + var ins, var del { + display: inline; + padding: 0; + margin: 0; + border: none; + border-radius: 0; + background: none; + } + + var ins { + color: var(--color-success); + } + + var del { + color: var(--color-danger); + } +} + +/* Button row inside uci-dialog */ +.uci-change-list + .right, +.uci-dialog .right { + margin-top: 16px; + display: flex; + gap: 8px; + justify-content: flex-end; +} diff --git a/luci-theme-glass/less/dark.less b/luci-theme-glass/less/dark.less new file mode 100644 index 000000000..c63391299 --- /dev/null +++ b/luci-theme-glass/less/dark.less @@ -0,0 +1,576 @@ +/* ========================================================================== + Dark Mode — luci-theme-glass + Compiled separately and injected by header.ut when dark mode is active. + ========================================================================== */ + +:root { + /* ---- Colors — Dark mode ---- */ + --color-primary: #0A84FF; + --color-primary-hover: #409cff; + --color-primary-active: #0066d6; + --color-primary-rgb: 10, 132, 255; + + --color-success: #30D158; + --color-warning: #FFD60A; + --color-danger: #FF453A; + --color-info: #64D2FF; + + --color-text: #f5f5f7; + --color-text-secondary: #98989d; + --color-text-tertiary: #636366; + --color-text-inverse: #1d1d1f; + + --color-bg: #1c1c1e; + --color-bg-elevated: #2c2c2e; + --color-bg-secondary: #3a3a3c; + + --color-border: rgba(255, 255, 255, 0.08); + --color-border-strong: rgba(255, 255, 255, 0.16); + --color-separator: rgba(84, 84, 88, 0.36); + + /* ---- Dark glassmorphism ---- */ + --glass-blur: 25px; + --glass-saturate: 180%; + --glass-bg: rgba(38, 38, 42, 0.55); + --glass-bg-hover: rgba(52, 52, 56, 0.65); + --glass-bg-strong: rgba(38, 38, 42, 0.72); + --glass-border: rgba(255, 255, 255, 0.12); + --glass-shadow: 0 2px 16px rgba(0, 0, 0, 0.20), + 0 0 1px rgba(0, 0, 0, 0.25); + --glass-shadow-lg: 0 8px 40px rgba(0, 0, 0, 0.30), + 0 0 1px rgba(0, 0, 0, 0.15); + + /* ---- Dark background ---- */ + --bg-gradient: linear-gradient(135deg, #2c2e3a 0%, #353848 50%, #2a2d38 100%); +} + +/* ---- Dark-mode-specific overrides ---- */ + +/* Inputs need darker backgrounds */ +.cbi-input-text, +.cbi-input-password, +input[type="text"], +input[type="password"], +input[type="number"], +textarea, +select { + background: rgba(44, 44, 46, 0.55); +} + +/* Login card */ +.login-card { + background-image: linear-gradient( + 160deg, + rgba(255, 255, 255, 0.10) 0%, + rgba(255, 255, 255, 0.07) 25%, + rgba(240, 248, 255, 0.08) 50%, + rgba(255, 255, 255, 0.06) 75%, + rgba(230, 240, 255, 0.07) 100% + ); + background-color: rgba(38, 38, 42, 0.55); + border-color: rgba(255, 255, 255, 0.14); + box-shadow: + 0 8px 40px rgba(0, 0, 0, 0.25), + 0 2px 8px rgba(0, 0, 0, 0.12), + inset 0 1px 0 rgba(255, 255, 255, 0.10), + inset 0 0 16px rgba(255, 255, 255, 0.06); +} + +.login-form .login-field input { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.15); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.08), + inset 0 0 4px rgba(255, 255, 255, 0.04); +} + +.login-btn { + background-image: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.08) 0%, + rgba(255, 255, 255, 0.06) 25%, + rgba(240, 248, 255, 0.07) 50%, + rgba(255, 255, 255, 0.05) 75%, + rgba(230, 240, 255, 0.06) 100% + ); + border-color: rgba(255, 255, 255, 0.15); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.30), + 0 1px 4px rgba(0, 0, 0, 0.15), + inset 0 0 0.5px rgba(255, 255, 255, 0.12), + inset 0 0 12px rgba(255, 255, 255, 0.15); + color: var(--color-text); +} + +/* Buttons — glass-ui dark pattern */ +.btn, +.cbi-button { + background-image: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.08) 0%, + rgba(255, 255, 255, 0.06) 25%, + rgba(240, 248, 255, 0.07) 50%, + rgba(255, 255, 255, 0.05) 75%, + rgba(230, 240, 255, 0.06) 100% + ); + background-color: transparent; + border-color: rgba(255, 255, 255, 0.15); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.30), + 0 1px 4px rgba(0, 0, 0, 0.15), + inset 0 0 0.5px rgba(255, 255, 255, 0.12), + inset 0 0 12px rgba(255, 255, 255, 0.15); + + &:hover { + opacity: 0.90; + background-color: transparent; + } + + &:active { + box-shadow: + inset 0 2px 4px rgba(0, 0, 0, 0.30), + inset 0 1px 2px rgba(0, 0, 0, 0.20); + } +} + +.cbi-button-negative, +.cbi-button-remove, +.cbi-button-reset { + background-image: linear-gradient( + 135deg, + rgba(255, 69, 58, 0.12) 0%, + rgba(255, 69, 58, 0.08) 50%, + rgba(255, 69, 58, 0.10) 100% + ); + background-color: transparent; + color: var(--color-danger); + border-color: rgba(255, 69, 58, 0.25); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.30), + 0 1px 4px rgba(0, 0, 0, 0.15), + inset 0 0 0.5px rgba(255, 255, 255, 0.08), + inset 0 0 8px rgba(255, 255, 255, 0.06); + + &:hover { + opacity: 0.90; + background-color: transparent; + } + + &:active { + box-shadow: + inset 0 2px 4px rgba(0, 0, 0, 0.30), + inset 0 1px 2px rgba(0, 0, 0, 0.20); + } +} + +/* Sidebar */ +#sidebar { + background-image: linear-gradient( + 160deg, + rgba(255, 255, 255, 0.10) 0%, + rgba(255, 255, 255, 0.07) 20%, + rgba(240, 248, 255, 0.08) 45%, + rgba(255, 255, 255, 0.06) 70%, + rgba(230, 240, 255, 0.07) 100% + ); + background-color: rgba(38, 38, 42, 0.55); + border-color: rgba(255, 255, 255, 0.14); + box-shadow: + 0 8px 40px rgba(0, 0, 0, 0.25), + 0 2px 8px rgba(0, 0, 0, 0.12), + inset 0 1px 0 rgba(255, 255, 255, 0.10), + inset 0 0 12px rgba(255, 255, 255, 0.06); +} + +/* Header bar */ +#header { + background-image: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.10) 0%, + rgba(255, 255, 255, 0.07) 25%, + rgba(240, 248, 255, 0.08) 50%, + rgba(255, 255, 255, 0.06) 75%, + rgba(230, 240, 255, 0.07) 100% + ); + border-color: rgba(255, 255, 255, 0.15); + box-shadow: + 0 2px 16px rgba(0, 0, 0, 0.20), + 0 1px 4px rgba(0, 0, 0, 0.10), + inset 0 1px 0 rgba(255, 255, 255, 0.10), + inset 0 0 8px rgba(255, 255, 255, 0.06); +} + +/* Page actions bar */ +.cbi-page-actions { + background-image: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.10) 0%, + rgba(255, 255, 255, 0.07) 25%, + rgba(240, 248, 255, 0.08) 50%, + rgba(255, 255, 255, 0.06) 75%, + rgba(230, 240, 255, 0.07) 100% + ); + border-color: rgba(255, 255, 255, 0.15); + box-shadow: + 0 2px 16px rgba(0, 0, 0, 0.20), + 0 1px 4px rgba(0, 0, 0, 0.10), + inset 0 1px 0 rgba(255, 255, 255, 0.10), + inset 0 0 8px rgba(255, 255, 255, 0.06); +} + +/* Nav hover & active */ +.nav-item:hover { + background: rgba(255, 255, 255, 0.06); +} + +.nav-item.active { + background: rgba(255, 255, 255, 0.15); + color: var(--color-text); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + box-shadow: + 0 2px 6px rgba(0, 0, 0, 0.30), + inset 0 1px 2px rgba(255, 255, 255, 0.12), + inset 0 0 8px rgba(255, 255, 255, 0.08); +} + +/* Sub-item active — slider handles highlight */ +.nav-sub .nav-item.active { + background: transparent; + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; +} + +/* Sliding selector — dark glass */ +.nav-sub .nav-slider { + background: rgba(255, 255, 255, 0.08); + border: 0.5px solid rgba(255, 255, 255, 0.12); + backdrop-filter: blur(12px) saturate(150%); + -webkit-backdrop-filter: blur(12px) saturate(150%); + box-shadow: + 0 1px 4px rgba(0, 0, 0, 0.20), + inset 0 1px 1px rgba(255, 255, 255, 0.08), + inset 0 0 4px rgba(255, 255, 255, 0.03); +} + +/* Header tabs */ +.header-tab { + &:hover { + background: rgba(255, 255, 255, 0.06); + color: var(--color-text); + } + &.active { + background: transparent; + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; + } +} + +/* Tab slider — dark glass */ +.tab-slider { + background: rgba(255, 255, 255, 0.10); + border: 0.5px solid rgba(255, 255, 255, 0.12); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + box-shadow: + 0 2px 6px rgba(0, 0, 0, 0.30), + inset 0 1px 2px rgba(255, 255, 255, 0.10), + inset 0 0 8px rgba(255, 255, 255, 0.06); +} + +/* Sub-nav bar */ +#header-sub { + background-image: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.10) 0%, + rgba(255, 255, 255, 0.07) 25%, + rgba(240, 248, 255, 0.08) 50%, + rgba(255, 255, 255, 0.06) 75%, + rgba(230, 240, 255, 0.07) 100% + ); + border-color: rgba(255, 255, 255, 0.15); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.20), + 0 1px 4px rgba(0, 0, 0, 0.10), + 0 0 0 0.5px rgba(255, 255, 255, 0.12) inset, + 0 0 12px rgba(255, 255, 255, 0.15) inset; +} + +.sub-tab { + &:hover { + background: rgba(255, 255, 255, 0.06); + } + + &.active { + background: transparent; + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; + } +} + +/* Header glass pills */ +.header-title, +#status-indicators, +.header-actions { + background-image: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.04) 0%, + rgba(255, 255, 255, 0.02) 50%, + rgba(255, 255, 255, 0.03) 100% + ); + border-color: rgba(255, 255, 255, 0.10); + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.25), + 0 1px 2px rgba(0, 0, 0, 0.12), + inset 0 0.5px 0 rgba(255, 255, 255, 0.06); +} + +/* Theme toggle */ +.theme-toggle:hover { + background: rgba(255, 255, 255, 0.08); +} + +.theme-toggle-float { + background-image: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.10) 0%, + rgba(255, 255, 255, 0.07) 25%, + rgba(240, 248, 255, 0.08) 50%, + rgba(255, 255, 255, 0.06) 75%, + rgba(230, 240, 255, 0.07) 100% + ); + border-color: rgba(255, 255, 255, 0.15); + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.18), + 0 1px 2px rgba(0, 0, 0, 0.08), + inset 0 0 0.5px rgba(255, 255, 255, 0.12), + inset 0 1px 6px rgba(255, 255, 255, 0.12); +} + +/* Tabs */ +.cbi-tabmenu { + background-image: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.10) 0%, + rgba(255, 255, 255, 0.07) 25%, + rgba(240, 248, 255, 0.08) 50%, + rgba(255, 255, 255, 0.06) 75%, + rgba(230, 240, 255, 0.07) 100% + ); + border-color: rgba(255, 255, 255, 0.15); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.20), + 0 1px 4px rgba(0, 0, 0, 0.10), + inset 0 0 0.5px rgba(255, 255, 255, 0.12), + inset 0 0 12px rgba(255, 255, 255, 0.15); + + li { + a:hover { + background: rgba(255, 255, 255, 0.06); + } + + &.cbi-tab a { + background: rgba(255, 255, 255, 0.15); + box-shadow: + 0 2px 6px rgba(0, 0, 0, 0.30), + inset 0 1px 2px rgba(255, 255, 255, 0.12), + inset 0 0 8px rgba(255, 255, 255, 0.08); + } + } +} + +/* Section fold button */ +.cbi-section.fade-in .cbi-title > div:last-child span:hover { + background: rgba(255, 255, 255, 0.08); +} + +/* Table row hover */ +table tbody tr:hover td, +.table .tr:hover .td { + background: rgba(255, 255, 255, 0.03); +} + +/* Pre / code */ +pre, +#syslog { + background: rgba(255, 255, 255, 0.04); +} + +/* Alert colors adjusted for dark */ +.alert-message, +.cbi-section-error { + background: rgba(100, 210, 255, 0.08); + border-color: rgba(100, 210, 255, 0.18); + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.20), + inset 0 1px 0 rgba(255, 255, 255, 0.08), + inset 0 0 6px rgba(255, 255, 255, 0.04); +} + +.alert-message.notice, +.alert-message.info { + background: rgba(100, 210, 255, 0.10); + border-color: rgba(100, 210, 255, 0.25); +} + +.alert-message.warning { + background: rgba(255, 214, 10, 0.10); + border-color: rgba(255, 214, 10, 0.25); +} + +.alert-message.error, +.cbi-section-error { + background: rgba(255, 69, 58, 0.10); + border-color: rgba(255, 69, 58, 0.25); +} + +.alert-message.success { + background: rgba(48, 209, 88, 0.10); + border-color: rgba(48, 209, 88, 0.25); +} + +/* Diagnostics & file browser in dark */ +#diag-rc-output, +.cbi-filebrowser { + background-image: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.10) 0%, + rgba(255, 255, 255, 0.07) 25%, + rgba(240, 248, 255, 0.08) 50%, + rgba(255, 255, 255, 0.06) 75%, + rgba(230, 240, 255, 0.07) 100% + ); + border-color: rgba(255, 255, 255, 0.15); + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.18), + 0 1px 2px rgba(0, 0, 0, 0.08), + inset 0 1px 0 rgba(255, 255, 255, 0.08), + inset 0 0 6px rgba(255, 255, 255, 0.04); +} + +/* Ifacebox zone header in dark — slightly brighter tint */ +.ifacebox-head[style*="background-color"] { + background-color: rgba(var(--zone-color-rgb), 0.20) !important; + border-bottom-color: rgba(var(--zone-color-rgb), 0.25) !important; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08); +} + +/* Zone badges in dark — tint zone color instead of opaque background */ +.zonebadge[style*="background-color"] { + background-color: rgba(var(--zone-color-rgb), 0.25) !important; + border-color: rgba(var(--zone-color-rgb), 0.30); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.06), + inset 0 0 4px rgba(255, 255, 255, 0.03); +} + +.zonebadge { + border-color: rgba(255, 255, 255, 0.08); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.06), + inset 0 0 4px rgba(255, 255, 255, 0.03); +} + +/* Ifacebox in dark */ +.ifacebadge { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.10); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.06), + inset 0 0 4px rgba(255, 255, 255, 0.03); +} + +/* Port grid boxes in dark — glass-ui dark card */ +div[style*="display:grid;grid-template-columns:repeat"] > .ifacebox { + background-image: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.10) 0%, + rgba(255, 255, 255, 0.07) 25%, + rgba(240, 248, 255, 0.08) 50%, + rgba(255, 255, 255, 0.06) 75%, + rgba(230, 240, 255, 0.07) 100% + ) !important; + border-color: rgba(255, 255, 255, 0.15) !important; + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.20), + 0 1px 4px rgba(0, 0, 0, 0.10), + 0 0 0 0.5px rgba(255, 255, 255, 0.12) inset, + 0 0 12px rgba(255, 255, 255, 0.15) inset !important; +} + +/* Network status table badges in dark */ +.network-status-table .ifacebox-body .ifacebadge { + background: rgba(255, 255, 255, 0.06); +} + +/* Dropdown open items hover */ +.cbi-dropdown[open] > ul.dropdown > li:hover { + background: rgba(255, 255, 255, 0.06); +} + +/* Progressbar */ +.cbi-progressbar { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.06); + + > div { + background: linear-gradient( + 135deg, + rgba(var(--color-primary-rgb), 0.65) 0%, + rgba(var(--color-primary-rgb), 0.50) 50%, + rgba(var(--color-primary-rgb), 0.60) 100% + ); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.15), + inset 0 0 6px rgba(255, 255, 255, 0.06); + } + + &::after { + color: var(--color-text); + text-shadow: 0 0 4px rgba(0, 0, 0, 0.40); + } +} + +/* Ifacebox-body itemlist labels */ +.ifacebox-body > span > .nowrap > strong { + color: var(--color-text-tertiary); +} + +/* UCI Changes dialog in dark */ +.uci-change-legend-label { + > ins { + background-color: rgba(48, 209, 88, 0.30); + border-color: rgba(48, 209, 88, 0.50); + } + > del { + background-color: rgba(255, 69, 58, 0.30); + border-color: rgba(255, 69, 58, 0.50); + } + > var { + background-color: rgba(100, 210, 255, 0.25); + border-color: rgba(100, 210, 255, 0.40); + } +} + +.uci-change-list { + ins { + background-color: rgba(48, 209, 88, 0.12); + border-color: rgba(48, 209, 88, 0.25); + } + del { + background-color: rgba(255, 69, 58, 0.12); + border-color: rgba(255, 69, 58, 0.25); + } + var { + background-color: rgba(100, 210, 255, 0.10); + border-color: rgba(100, 210, 255, 0.18); + } +} + +/* Selection */ +::selection { + background: rgba(var(--color-primary-rgb), 0.35); +} diff --git a/luci-theme-glass/less/glass.less b/luci-theme-glass/less/glass.less new file mode 100644 index 000000000..56aefd422 --- /dev/null +++ b/luci-theme-glass/less/glass.less @@ -0,0 +1,55 @@ +/* ========================================================================== + Glassmorphism Mixins — luci-theme-glass + ========================================================================== */ + +/* Standard glass panel — default for cards, sections, dropdowns */ +.glass-effect() { + background: var(--glass-bg); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + border: 0.5px solid var(--glass-border); + box-shadow: var(--glass-shadow); +} + +/* Frosted glass — stronger blur, higher opacity (sidebar, login card) */ +.glass-frosted() { + background: var(--glass-bg-strong); + backdrop-filter: blur(30px) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(30px) saturate(var(--glass-saturate)); + border: 0.5px solid var(--glass-border); + box-shadow: var(--glass-shadow-lg); +} + +/* Subtle glass — minimal effect for secondary/nested elements */ +.glass-subtle() { + background: rgba(255, 255, 255, 0.40); + backdrop-filter: blur(10px) saturate(120%); + -webkit-backdrop-filter: blur(10px) saturate(120%); + border: 0.5px solid rgba(255, 255, 255, 0.25); +} + +/* Glass hover state */ +.glass-hover() { + background: var(--glass-bg-hover); + box-shadow: var(--glass-shadow-lg); +} + +/* Inner glow — top highlight for glass panels */ +.glass-inner-glow() { + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 1px; + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.6), + transparent + ); + border-radius: inherit; + pointer-events: none; + } +} diff --git a/luci-theme-glass/less/layout.less b/luci-theme-glass/less/layout.less new file mode 100644 index 000000000..f134422e8 --- /dev/null +++ b/luci-theme-glass/less/layout.less @@ -0,0 +1,798 @@ +/* ========================================================================== + Layout — Sidebar, Header, Content + ========================================================================== */ + +/* ---- Background layer ---- */ +#bg-wrap { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + background: var(--bg-gradient); + background-size: cover; + background-position: center; + + img, video { + width: 100%; + height: 100%; + object-fit: cover; + } +} + +/* ---- Sidebar ---- */ +#sidebar { + position: fixed; + top: var(--sidebar-gap); + left: var(--sidebar-gap); + width: var(--sidebar-width); + height: calc(100vh - var(--sidebar-gap) * 2); + z-index: var(--z-sidebar); + /* True glassmorphism — multi-stop gradient, stronger for readability */ + background-image: linear-gradient( + 160deg, + rgba(255, 255, 255, 0.22) 0%, + rgba(255, 255, 255, 0.18) 20%, + rgba(245, 250, 255, 0.20) 45%, + rgba(255, 255, 255, 0.16) 70%, + rgba(240, 245, 255, 0.19) 100% + ); + background-color: rgba(255, 255, 255, 0.62); + backdrop-filter: blur(30px) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(30px) saturate(var(--glass-saturate)); + border: 0.5px solid rgba(255, 255, 255, 0.40); + box-shadow: + 0 8px 40px rgba(0, 0, 0, 0.08), + 0 2px 8px rgba(0, 0, 0, 0.04), + inset 0 1px 0 rgba(255, 255, 255, 0.50), + inset 0 0 12px rgba(255, 255, 255, 0.15); + border-radius: var(--radius-xl); + display: flex; + flex-direction: column; + transition: transform var(--transition-slow); + overflow: hidden; +} + +.sidebar-header { + display: flex; + align-items: center; + padding: var(--space-lg) var(--space-xl); + height: var(--header-height); + border-bottom: 0.5px solid var(--color-separator); + + .logo { + width: 32px; + height: 32px; + flex-shrink: 0; + } + + .brand { + margin-left: var(--space-md); + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semibold); + color: var(--color-text); + white-space: nowrap; + overflow: hidden; + } + +} + +.sidebar-footer { + padding: var(--space-md) var(--space-xl); + border-top: 0.5px solid var(--color-separator); + text-align: center; + + .sidebar-version { + font-size: 0.75rem; + color: var(--color-text-tertiary); + } +} + +/* ---- Sidebar navigation ---- */ +.sidebar-nav { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: var(--space-sm) 0; + + &::-webkit-scrollbar { + width: 4px; + } + &::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.15); + border-radius: var(--radius-full); + } +} + +.nav-group { + margin-bottom: var(--space-xs); + + .nav-group-title { + padding: var(--space-sm) var(--space-xl); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + color: var(--color-text-tertiary); + text-transform: uppercase; + letter-spacing: 0.05em; + } +} + +.nav-item { + display: flex; + align-items: center; + padding: 0.55rem var(--space-xl); + margin: 1px var(--space-sm); + border-radius: var(--radius-md); + font-size: var(--font-size-lg); + color: var(--color-text); + cursor: pointer; + transition: background var(--transition-fast), color var(--transition-fast); + text-decoration: none; + + &:hover { + background: rgba(0, 0, 0, 0.04); + color: var(--color-text); + } + + &.active { + background: rgba(255, 255, 255, 0.45); + color: var(--color-text); + font-weight: var(--font-weight-medium); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.08), + inset 0 1px 1px rgba(255, 255, 255, 0.60), + inset 0 0 6px rgba(255, 255, 255, 0.15); + } + + .nav-icon { + width: 20px; + height: 20px; + margin-right: var(--space-md); + flex-shrink: 0; + opacity: 0.65; + } + + &.active .nav-icon { + opacity: 1; + } + + .nav-label { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .nav-arrow { + margin-left: auto; + width: 16px; + height: 16px; + opacity: 0.4; + transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), opacity var(--transition-fast); + } + + &.open .nav-arrow { + transform: rotate(90deg); + opacity: 0.6; + } +} + +.nav-sub { + position: relative; + overflow: hidden; + max-height: 0; + opacity: 0; + transition: max-height 0.35s cubic-bezier(0.16, 1, 0.3, 1), + opacity 0.25s ease; + + &.open { + /* Upper bound for the expand animation. Must exceed the tallest submenu + encountered in the wild (System commonly reaches ~14 items once third- + party apps like theme-config, scheduled-tasks, etc. are installed). */ + max-height: 2000px; + opacity: 1; + } + + .nav-item { + padding-left: calc(var(--space-xl) + 32px); + font-size: var(--font-size-base); + transform: translateX(-8px); + opacity: 0; + transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), + opacity 0.2s ease, + color var(--transition-fast); + + /* Sub-item active state — no background, slider handles it */ + &.active { + background: transparent; + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; + font-weight: var(--font-weight-medium); + } + } + + &.open .nav-item { + transform: translateX(0); + opacity: 1; + } + + // Stagger sub-items (nth-of-type since slider is first child) + &.open .nav-item:nth-of-type(1) { transition-delay: 0.03s; } + &.open .nav-item:nth-of-type(2) { transition-delay: 0.06s; } + &.open .nav-item:nth-of-type(3) { transition-delay: 0.09s; } + &.open .nav-item:nth-of-type(4) { transition-delay: 0.12s; } + &.open .nav-item:nth-of-type(5) { transition-delay: 0.15s; } + &.open .nav-item:nth-of-type(6) { transition-delay: 0.18s; } + &.open .nav-item:nth-of-type(7) { transition-delay: 0.21s; } + &.open .nav-item:nth-of-type(8) { transition-delay: 0.24s; } + &.open .nav-item:nth-of-type(n+9) { transition-delay: 0.27s; } + + .nav-slider { + position: absolute; + left: var(--space-sm); + right: var(--space-sm); + top: 0; + height: 0; + opacity: 0; + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.35); + backdrop-filter: blur(12px) saturate(150%); + -webkit-backdrop-filter: blur(12px) saturate(150%); + border: 0.5px solid rgba(255, 255, 255, 0.45); + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.04), + inset 0 1px 1px rgba(255, 255, 255, 0.35), + inset 0 0 4px rgba(255, 255, 255, 0.10); + transition: top 0.3s cubic-bezier(0.16, 1, 0.3, 1), + height 0.2s cubic-bezier(0.16, 1, 0.3, 1), + opacity 0.2s ease; + z-index: 0; + pointer-events: none; + } + + .nav-item { + position: relative; + z-index: 1; + } +} + +/* ---- Sidebar user area ---- */ +.sidebar-user { + padding: var(--space-lg) var(--space-xl); + border-top: 0.5px solid var(--color-separator); + display: flex; + align-items: center; + + .user-avatar { + width: 32px; + height: 32px; + border-radius: var(--radius-full); + background: var(--color-bg-secondary); + display: flex; + align-items: center; + justify-content: center; + font-weight: var(--font-weight-semibold); + font-size: var(--font-size-sm); + color: var(--color-text-secondary); + flex-shrink: 0; + } + + .user-info { + margin-left: var(--space-md); + overflow: hidden; + + .user-name { + font-weight: var(--font-weight-medium); + font-size: var(--font-size-sm); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .user-host { + font-size: var(--font-size-sm); + color: var(--color-text-tertiary); + } + } +} + +/* ---- Top header nav bar ---- */ +#header { + position: fixed; + top: var(--sidebar-gap); + left: calc(var(--sidebar-width) + var(--sidebar-gap) * 2); + right: var(--sidebar-gap); + height: 44px; + z-index: var(--z-header); + border-radius: var(--radius-lg); + display: flex; + align-items: center; + padding: 4px 4px 4px 6px; + gap: var(--space-xs); + transition: left var(--transition-slow); + + /* True glassmorphism — multi-stop gradient like sub-nav */ + background-image: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.15) 0%, + rgba(255, 255, 255, 0.12) 25%, + rgba(240, 248, 255, 0.13) 50%, + rgba(255, 255, 255, 0.11) 75%, + rgba(230, 240, 255, 0.12) 100% + ); + background-color: transparent; + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + border: 0.5px solid rgba(255, 255, 255, 0.35); + box-shadow: + 0 2px 16px rgba(0, 0, 0, 0.06), + 0 1px 4px rgba(0, 0, 0, 0.03), + inset 0 1px 0 rgba(255, 255, 255, 0.40), + inset 0 0 8px rgba(255, 255, 255, 0.12); +} + +/* ---- Header top-level tabs ---- */ +#header-nav, +#header-tabs { + display: flex; + align-items: center; + position: relative; +} + +.header-sep { + width: 1px; + height: 18px; + background: var(--color-separator); + margin: 0 var(--space-xs); + flex-shrink: 0; +} + +.header-tab { + padding: 6px var(--space-lg); + border-radius: var(--radius-md); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + text-decoration: none; + transition: background var(--transition-fast), color var(--transition-fast), box-shadow var(--transition-fast); + white-space: nowrap; + + &:hover { + background: rgba(0, 0, 0, 0.04); + color: var(--color-text); + } + + &.active { + color: var(--color-text); + background: transparent; + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; + } +} + +/* Horizontal sliding selector for header tabs */ +.tab-slider { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 0; + opacity: 0; + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.45); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + box-shadow: + 0 1px 3px rgba(0, 0, 0, 0.08), + inset 0 1px 1px rgba(255, 255, 255, 0.60), + inset 0 0 6px rgba(255, 255, 255, 0.15); + transition: left 0.3s cubic-bezier(0.16, 1, 0.3, 1), + width 0.2s cubic-bezier(0.16, 1, 0.3, 1), + opacity 0.2s ease; + z-index: 0; + pointer-events: none; +} + +.header-tab, +.sub-tab { + position: relative; + z-index: 1; +} + +/* ---- Status indicators (center of header) ---- */ +#status-indicators { + margin: 0 auto; + display: flex; + align-items: center; + gap: var(--space-sm); + padding: 4px var(--space-md); + .header-glass-pill(); + + &:empty { + display: none; + } + + [data-indicator] { + display: flex; + align-items: center; + gap: 4px; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + white-space: nowrap; + cursor: default; + transition: color 0.3s ease; + + .indicator-icon { + width: 14px; + height: 14px; + flex-shrink: 0; + opacity: 0.6; + transition: color 0.3s ease, opacity 0.3s ease; + } + + /* Level colors — neutral by default, color only when noteworthy */ + &[data-level="warn"] { + color: #FF9500; + .indicator-icon { opacity: 0.85; } + } + &[data-level="crit"] { + color: #FF3B30; + .indicator-icon { opacity: 0.9; } + } + &[data-level="active"] { + color: #007AFF; + .indicator-icon { opacity: 0.8; } + } + &[data-level="busy"] { + color: #AF52DE; + .indicator-icon { opacity: 0.85; } + } + } + + [data-indicator] + [data-indicator] { + border-left: 0.5px solid rgba(128, 128, 128, 0.25); + padding-left: var(--space-sm); + } +} + +/* ---- Glass pill badges (title + indicators) ---- */ +.header-glass-pill() { + height: 2.25rem; + display: flex; + align-items: center; + border-radius: var(--radius-md); + background-image: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.15) 0%, + rgba(255, 255, 255, 0.10) 25%, + rgba(240, 248, 255, 0.12) 50%, + rgba(255, 255, 255, 0.08) 75%, + rgba(230, 240, 255, 0.10) 100% + ); + background-color: transparent; + border: 0.5px solid rgba(255, 255, 255, 0.18); + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.06), + 0 1px 2px rgba(0, 0, 0, 0.03), + 0 0 0 0.5px rgba(255, 255, 255, 0.08) inset, + 0 1px 6px rgba(255, 255, 255, 0.10) inset; + backdrop-filter: blur(12px) saturate(180%); + -webkit-backdrop-filter: blur(12px) saturate(180%); +} + +.header-title { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + color: var(--color-text); + white-space: nowrap; + cursor: default; + padding: 4px var(--space-md); + .header-glass-pill(); +} + +.header-actions { + margin-left: auto; + flex-shrink: 0; + display: flex; + align-items: center; + gap: var(--space-sm); + padding: 4px var(--space-md); + .header-glass-pill(); + + &:empty { + display: none; + } + + [data-indicator] { + display: flex; + align-items: center; + gap: 4px; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + white-space: nowrap; + cursor: default; + + &[data-style="active"] { + color: var(--color-primary); + } + } +} + +/* ---- Theme toggle button ---- */ +.theme-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + border: none; + border-radius: var(--radius-sm); + background: none; + color: var(--color-text-secondary); + cursor: pointer; + transition: color var(--transition-fast); + flex-shrink: 0; + + &:hover { + color: var(--color-text); + } + + &:active { + transform: scale(0.92); + } + + .theme-icon { + width: 15px; + height: 15px; + display: none; + } +} + +/* Show only the icon matching current theme */ +html[data-theme="auto"] .theme-icon-auto, +html:not([data-theme]) .theme-icon-auto { + display: block; +} +html[data-theme="light"] .theme-icon-light { + display: block; +} +html[data-theme="dark"] .theme-icon-dark { + display: block; +} + +/* Floating variant for login page */ +.theme-toggle-float { + position: fixed; + top: var(--space-lg); + right: var(--space-lg); + z-index: var(--z-dropdown); + width: 36px; + height: 36px; + border-radius: var(--radius-md); + .header-glass-pill(); + + .theme-icon { + width: 18px; + height: 18px; + } +} + +/* Mobile hamburger */ +.menu-toggle { + display: none; + background: none; + border: none; + cursor: pointer; + padding: var(--space-sm); + margin-right: var(--space-md); + color: var(--color-text); + + svg { + width: 22px; + height: 22px; + } +} + +/* ---- Sub-navigation bar (CBI tabs) ---- */ +#header-sub { + position: fixed; + top: calc(var(--sidebar-gap) + 44px + 4px); + left: calc(var(--sidebar-width) + var(--sidebar-gap) * 2); + width: fit-content; + max-width: calc(100vw - var(--sidebar-width) - var(--sidebar-gap) * 3); + height: 2.25rem; + z-index: var(--z-header); + border-radius: var(--radius-md); + display: none; + align-items: center; + padding: 3px 4px; + gap: 2px; + overflow-x: auto; + transition: left var(--transition-slow); + + /* Glass tab-list background — from glass-ui */ + background-image: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.15) 0%, + rgba(255, 255, 255, 0.12) 25%, + rgba(240, 248, 255, 0.13) 50%, + rgba(255, 255, 255, 0.11) 75%, + rgba(230, 240, 255, 0.12) 100% + ); + background-color: transparent; + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + border: 0.5px solid rgba(255, 255, 255, 0.12); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.08), + 0 1px 4px rgba(0, 0, 0, 0.04), + 0 0 0 0.5px rgba(255, 255, 255, 0.08) inset, + 0 0 12px rgba(255, 255, 255, 0.10) inset; + + /* Hide scrollbar */ + &::-webkit-scrollbar { + display: none; + } + scrollbar-width: none; + + &.active { + display: inline-flex; + } +} + +/* Sub-nav tab items — glass-ui TabsTrigger pattern */ +.sub-tab { + padding: 4px var(--space-md); + border-radius: var(--radius-sm); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + text-decoration: none; + white-space: nowrap; + cursor: pointer; + transition: background var(--transition-fast), color var(--transition-fast), + box-shadow var(--transition-fast), backdrop-filter var(--transition-fast); + + &:hover { + background: rgba(0, 0, 0, 0.04); + color: var(--color-text); + } + + &.active { + color: var(--color-text); + background: transparent; + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; + } +} + +/* Sub-tab slider — inset to match sub-tab content area */ +#header-sub > .tab-slider { + border-radius: var(--radius-sm); + top: 3px; + bottom: 3px; + height: auto; +} + +/* ---- Page transition ---- */ +@keyframes page-enter { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* ---- Main content area ---- */ +#content-wrap { + margin-left: calc(var(--sidebar-width) + var(--sidebar-gap) * 2); + margin-top: calc(44px + var(--sidebar-gap) * 2); + min-height: calc(100vh - 44px - var(--sidebar-gap) * 2); + padding: var(--content-padding); + transition: margin-left var(--transition-slow); + animation: page-enter 0.3s cubic-bezier(0.16, 1, 0.3, 1) both; +} + +/* JS view content — fade in when LuCI replaces the spinner */ +#view > :not(.spinning) { + animation: page-enter 0.25s cubic-bezier(0.16, 1, 0.3, 1) both; +} + +/* Style LuCI's "Loading view..." spinner as a centered glass card */ +#view > .spinning { + position: fixed; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); + padding: var(--space-lg) var(--space-xl); + border-radius: var(--radius-lg); + .glass-effect(); + z-index: 100; +} + +/* Fade-out before navigation */ +body.page-leaving #content-wrap { + opacity: 0; + transform: translateY(-8px); + transition: opacity 0.15s ease, transform 0.15s ease; +} + +/* Push content down when sub-nav is visible */ +body.has-sub-nav #content-wrap { + margin-top: calc(44px + 36px + var(--sidebar-gap) * 2 + 4px); +} + +#content { + max-width: var(--content-max-width); + margin: 0 auto; +} + +/* Mobile overlay */ +.sidebar-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.3); + z-index: calc(var(--z-sidebar) - 1); + opacity: 0; + transition: opacity var(--transition-normal); + + &.active { + display: block; + opacity: 1; + } +} + +/* ---- Page title ---- */ +.page-title { + margin-bottom: var(--space-xl); + + h2 { + font-size: var(--font-size-xl); + font-weight: var(--font-weight-bold); + } + + .page-description { + margin-top: var(--space-xs); + color: var(--color-text-secondary); + font-size: var(--font-size-sm); + } +} + +/* ---- Footer ---- */ +.glass-footer { + margin-left: calc(var(--sidebar-width) + var(--sidebar-gap) * 2); + padding: var(--space-lg) var(--content-padding); + text-align: right; + font-size: var(--font-size-sm); + color: var(--color-text-tertiary); + transition: margin-left var(--transition-slow); + + a { + color: var(--color-text-tertiary); + text-decoration: none; + + &:hover { + color: var(--color-text-secondary); + } + } + + .footer-sep { + margin: 0 var(--space-sm); + opacity: 0.4; + } +} diff --git a/luci-theme-glass/less/normalize.less b/luci-theme-glass/less/normalize.less new file mode 100644 index 000000000..002275ba4 --- /dev/null +++ b/luci-theme-glass/less/normalize.less @@ -0,0 +1,129 @@ +/* ========================================================================== + Normalize / Reset — luci-theme-glass + Minimal reset for LuCI pages. + ========================================================================== */ + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: var(--font-size-base); + line-height: var(--line-height); + -webkit-text-size-adjust: 100%; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + /* Glass scrollbar */ + scrollbar-width: thin; + scrollbar-color: rgba(128, 128, 128, 0.35) transparent; +} + +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: rgba(128, 128, 128, 0.35); + border-radius: 3px; + + &:hover { + background: rgba(128, 128, 128, 0.55); + } +} + +body { + font-family: var(--font-sans); + font-size: var(--font-size-base); + color: var(--color-text); + background: var(--color-bg); + min-height: 100vh; + overflow-x: hidden; +} + +a { + color: var(--color-primary); + text-decoration: none; + transition: color var(--transition-fast); + + &:hover { + color: var(--color-primary-hover); + } +} + +img { + max-width: 100%; + height: auto; +} + +table { + border-collapse: collapse; + border-spacing: 0; + width: 100%; +} + +input, +select, +textarea, +button { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +code, +pre { + font-family: var(--font-mono); + font-size: var(--font-size-sm); +} + +hr { + border: none; + border-top: 0.5px solid var(--color-border); + margin: var(--space-md) 0; +} + +p { + margin-bottom: var(--space-sm); +} + +ul, ol { + padding-left: 1.5em; + margin: var(--space-sm) 0; +} + +li { + margin-bottom: 2px; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: var(--font-weight-semibold); + line-height: 1.25; + color: var(--color-text); +} + +h1 { font-size: var(--font-size-2xl); } +h2 { font-size: var(--font-size-xl); } +h3 { font-size: 1.125rem; } +h4 { font-size: 1rem; } + +::selection { + background: rgba(var(--color-primary-rgb), 0.25); +} + +/* LuCI injects at the top of every + CBI form. Without this rule the input renders as an unstyled browser + button (seen in PassWall, OpenAppFilter, etc). Both bootstrap and argon + themes ship this utility rule. */ +.hidden { + display: none; +} diff --git a/luci-theme-glass/less/page-fix.less b/luci-theme-glass/less/page-fix.less new file mode 100644 index 000000000..19772dcdd --- /dev/null +++ b/luci-theme-glass/less/page-fix.less @@ -0,0 +1,553 @@ +/* ========================================================================== + Page Fixes — LuCI-specific overrides and quirks + ========================================================================== */ + +/* LuCI wraps everything in #maincontent */ +#maincontent { + padding: 0; +} + +/* ---- Interface boxes (general) ---- */ +.ifacebox { + .glass-effect(); + display: inline-flex; + flex-direction: column; + border-radius: var(--radius-md); + padding: 0; + margin-bottom: var(--space-md); + line-height: 1.25; + overflow: visible; + min-width: 100px; +} + +.ifacebox-head { + font-weight: var(--font-weight-semibold); + padding: var(--space-sm) var(--space-lg); + margin: 0; + border-bottom: 0.5px solid var(--color-separator); + + /* Zone-colored header — tinted glass instead of opaque */ + &[style*="background-color"] { + background-color: rgba(var(--zone-color-rgb), 0.50) !important; + border-radius: var(--radius-md) var(--radius-md) 0 0; + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + border-bottom: 0.5px solid rgba(var(--zone-color-rgb), 0.20); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.20); + } + + &.active { + background: linear-gradient( + 135deg, + rgba(var(--color-primary-rgb), 0.75) 0%, + rgba(var(--color-primary-rgb), 0.60) 50%, + rgba(var(--color-primary-rgb), 0.70) 100% + ); + color: var(--color-text-inverse); + padding: var(--space-sm) var(--space-lg); + border-bottom: none; + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.20), + inset 0 0 6px rgba(255, 255, 255, 0.08); + } +} + +.ifacebox-body { + line-height: 1.5; + font-size: var(--font-size-sm); + padding: var(--space-sm) var(--space-lg) var(--space-lg); + + /* L.itemlist output — key-value pairs */ + > span { + display: block; + padding: var(--space-xs) 0; + + > .nowrap { + display: inline-block; + margin-right: var(--space-md); + margin-bottom: 2px; + + > strong { + color: var(--color-text-secondary); + font-weight: var(--font-weight-medium); + } + } + } + + /* Device badge row */ + > div { + margin-top: var(--space-sm); + } +} + +/* ---- Port status grid (29_ports.js) ---- */ +div[style*="display:grid;grid-template-columns:repeat"] { + gap: var(--space-md) !important; + margin-bottom: var(--space-lg) !important; + + > .ifacebox { + margin: 0 !important; + padding: 0 !important; + text-align: center; + border-radius: var(--radius-md) !important; + /* Glass-ui card pattern */ + background-image: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.15) 0%, + rgba(255, 255, 255, 0.12) 25%, + rgba(240, 248, 255, 0.13) 50%, + rgba(255, 255, 255, 0.11) 75%, + rgba(230, 240, 255, 0.12) 100% + ) !important; + background-color: transparent !important; + backdrop-filter: blur(10px) saturate(180%) !important; + -webkit-backdrop-filter: blur(10px) saturate(180%) !important; + border: 0.5px solid rgba(255, 255, 255, 0.12) !important; + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.08), + 0 1px 4px rgba(0, 0, 0, 0.04), + 0 0 0 0.5px rgba(255, 255, 255, 0.08) inset, + 0 0 12px rgba(255, 255, 255, 0.10) inset !important; + + > .ifacebox-head { + padding: var(--space-xs) var(--space-sm); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + margin-bottom: 0; + } + + > .ifacebox-body { + padding: var(--space-xs) var(--space-sm); + font-size: var(--font-size-sm); + line-height: 1.3; + + img { + width: 28px; + height: 28px; + margin: 2px auto; + } + } + + /* Zone color bar */ + > .ifacebox-head.cbi-tooltip-container { + padding: 0; + border-bottom: none; + + .zonebadge { + border-radius: 0; + padding: 0; + } + } + } +} + +/* ---- Network status table (30_network.js) ---- */ +.network-status-table { + display: flex; + flex-wrap: wrap; + gap: var(--space-md); + + .ifacebox { + flex: 1 1 300px; + margin: 0; + } + + .ifacebox-body { + display: flex; + flex-direction: column; + gap: var(--space-xs); + + > div { + display: flex; + flex-wrap: wrap; + gap: var(--space-sm); + } + + .ifacebadge { + flex: 1 1 auto; + min-width: 200px; + + > span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + } +} + +/* ---- Interface badges ---- */ +.ifacebadge { + display: inline-flex; + align-items: center; + gap: var(--space-xs); + padding: 2px var(--space-sm); + margin-right: 0.4em; + min-width: max-content; + background: rgba(255, 255, 255, 0.40); + backdrop-filter: blur(8px) saturate(180%); + -webkit-backdrop-filter: blur(8px) saturate(180%); + border: 0.5px solid rgba(255, 255, 255, 0.20); + border-radius: var(--radius-sm); + font-size: var(--font-size-sm); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.30), + inset 0 0 4px rgba(255, 255, 255, 0.08); + + img { + width: 16px; + height: 16px; + } +} + +/* Zone badges */ +.zonebadge { + display: inline-block; + padding: 2px var(--space-sm); + border-radius: var(--radius-sm); + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + border: 0.5px solid rgba(255, 255, 255, 0.15); + backdrop-filter: blur(6px) saturate(160%); + -webkit-backdrop-filter: blur(6px) saturate(160%); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.20), + inset 0 0 4px rgba(255, 255, 255, 0.06); +} + +.zonebadge .ifacebadge { + margin: 1px 2px; + padding: 2px 4px; +} + +/* ---- Tooltips ---- */ +.cbi-tooltip-container { + position: relative; +} + +.cbi-tooltip { + display: none; + position: absolute; + z-index: var(--z-toast); + left: 0; + bottom: 100%; + margin-bottom: var(--space-xs); + .glass-effect(); + border-radius: var(--radius-sm); + padding: var(--space-sm) var(--space-md); + font-size: var(--font-size-sm); + max-width: 300px; + white-space: normal; + line-height: 1.5; + + &.left { + left: auto; + right: 0; + } +} + +.cbi-tooltip-container:hover > .cbi-tooltip { + display: block; +} + +/* System log */ +#syslog { + font-family: var(--font-mono); + font-size: var(--font-size-sm); + background: rgba(0, 0, 0, 0.04); + border-radius: var(--radius-md); + padding: var(--space-lg); + overflow-y: hidden; + width: 100%; + white-space: pre-wrap; + word-break: break-all; +} + +/* Terminal / command output */ +pre { + background: rgba(0, 0, 0, 0.04); + border-radius: var(--radius-md); + padding: var(--space-lg); + overflow-x: auto; + font-family: var(--font-mono); + font-size: var(--font-size-sm); +} + +/* Flatten pre inside modals — avoid box-in-box */ +.modal pre { + background: none; + border: none; + box-shadow: none; + border-radius: 0; + padding: var(--space-sm) 0; +} + +/* Native progress element (package manager, etc.) */ +progress { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 8px; + border: none; + border-radius: var(--radius-full); + overflow: hidden; + background: rgba(0, 0, 0, 0.06); + + &::-webkit-progress-bar { + background: rgba(0, 0, 0, 0.06); + border-radius: var(--radius-full); + } + + &::-webkit-progress-value { + background: linear-gradient( + 135deg, + rgba(var(--color-primary-rgb), 0.75) 0%, + rgba(var(--color-primary-rgb), 0.60) 50%, + rgba(var(--color-primary-rgb), 0.70) 100% + ); + border-radius: var(--radius-full); + } + + &::-moz-progress-bar { + background: linear-gradient( + 135deg, + rgba(var(--color-primary-rgb), 0.75) 0%, + rgba(var(--color-primary-rgb), 0.60) 50%, + rgba(var(--color-primary-rgb), 0.70) 100% + ); + border-radius: var(--radius-full); + } +} + +/* ACL/UBUS error page styling */ +#error-message { + text-align: center; + padding: var(--space-3xl); + color: var(--color-text-secondary); +} + +/* LuCI diagnostics ping/traceroute output */ +#diag-rc-output { + border-radius: var(--radius-md); + padding: var(--space-lg); + background-image: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.15) 0%, + rgba(255, 255, 255, 0.12) 25%, + rgba(240, 248, 255, 0.13) 50%, + rgba(255, 255, 255, 0.11) 75%, + rgba(230, 240, 255, 0.12) 100% + ); + background-color: transparent; + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + border: 0.5px solid rgba(255, 255, 255, 0.12); + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.06), + 0 1px 2px rgba(0, 0, 0, 0.03), + inset 0 1px 0 rgba(255, 255, 255, 0.30), + inset 0 0 6px rgba(255, 255, 255, 0.08); +} + +/* File upload */ +.cbi-filebrowser { + border-radius: var(--radius-md); + padding: var(--space-lg); + background-image: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.15) 0%, + rgba(255, 255, 255, 0.12) 25%, + rgba(240, 248, 255, 0.13) 50%, + rgba(255, 255, 255, 0.11) 75%, + rgba(230, 240, 255, 0.12) 100% + ); + background-color: transparent; + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate)); + border: 0.5px solid rgba(255, 255, 255, 0.12); + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.06), + 0 1px 2px rgba(0, 0, 0, 0.03), + inset 0 1px 0 rgba(255, 255, 255, 0.30), + inset 0 0 6px rgba(255, 255, 255, 0.08); +} + +/* Apply config spinner */ +#cbi_apply_overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.25); + z-index: var(--z-overlay); + display: flex; + align-items: center; + justify-content: center; +} + +#cbi_apply_overlay .spinning { + width: 40px; + height: 40px; + padding-left: 0 !important; + border: 3px solid var(--color-bg-secondary); + border-top-color: var(--color-primary); + border-radius: 50%; + animation: spin 1s linear infinite; + + &::before { + display: none; + } +} + +/* ---- Utility classes ---- */ +.nowrap { + white-space: nowrap; +} + +.left { + text-align: left; +} + +.center { + text-align: center; +} + +.right { + text-align: right; +} + +.hide-xs { + @media (max-width: 640px) { + display: none !important; + } +} + +/* ---- Standalone section headings (DHCP, WiFi assoclist) ---- */ +#maincontent h3, +#view h3 { + font-size: var(--font-size-lg); + font-weight: var(--font-weight-semibold); + margin: var(--space-xl) 0 var(--space-md); +} + +/* ---- Table sort indicators ---- */ +.th[data-sort-direction] { + cursor: pointer; + user-select: none; + + &::after { + content: ''; + display: inline-block; + width: 0; + height: 0; + margin-left: var(--space-xs); + vertical-align: middle; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + } + + &[data-sort-direction="asc"]::after { + border-bottom: 5px solid var(--color-text-secondary); + } + + &[data-sort-direction="desc"]::after { + border-top: 5px solid var(--color-text-secondary); + } +} + +.th[data-sortable-row] { + cursor: pointer; +} + +/* ---- Table placeholder (empty state) ---- */ +.tr.placeholder .td { + text-align: center; + padding: var(--space-2xl) var(--space-lg); + color: var(--color-text-tertiary); + font-style: italic; +} + +/* ---- Software / Package Manager page ---- */ +[data-page="admin-system-opkg"], +[data-page="admin-system-package-manager"] { + /* Action buttons — glass pill style */ + div.btn { + display: inline-flex; + padding: var(--space-sm) var(--space-lg); + } + + /* Override opkg.js inline CSS for controls layout */ + .controls { + display: flex !important; + flex-wrap: wrap !important; + justify-content: flex-start !important; + gap: var(--space-md) var(--space-lg) !important; + margin: 0 0 var(--space-lg) 0 !important; + } + + /* All control sections — override inline flex: 1 1 33% */ + .controls > div { + padding: 0 !important; + white-space: normal !important; + display: flex !important; + flex-wrap: wrap !important; + align-items: flex-end !important; + } + + /* Disk space — full width row */ + .controls > div:first-child { + flex: 1 1 100% !important; + } + + /* Filter — share row, grow equally */ + .controls > div:nth-child(2) { + flex: 1 1 200px !important; + } + + /* Download / install — share row, grow equally */ + .controls > div:nth-child(3) { + flex: 1 1 280px !important; + } + + /* Actions — own row */ + .controls > div:nth-child(4) { + flex: 1 1 100% !important; + } + + /* Translation filter — own row */ + .controls > div:nth-child(5) { + flex: 1 1 100% !important; + } + + /* Labels — block above their control group */ + .controls > div > label { + flex-basis: 100% !important; + min-width: 0 !important; + margin-bottom: var(--space-xs) !important; + font-weight: var(--font-weight-medium) !important; + font-size: var(--font-size-sm) !important; + color: var(--color-text-secondary) !important; + } + + /* Control groups — input + button inline */ + .controls .control-group { + display: flex !important; + gap: var(--space-sm) !important; + flex: 1 1 auto !important; + align-items: center !important; + } + + .controls .control-group input[type="text"] { + flex: 1 1 auto !important; + min-width: 120px !important; + width: auto !important; + } + + /* Action buttons — inline row with gap */ + .controls > div:nth-child(4) > .control-group { + gap: var(--space-sm) !important; + } +} diff --git a/luci-theme-glass/less/responsive.less b/luci-theme-glass/less/responsive.less new file mode 100644 index 000000000..c5ff6dc31 --- /dev/null +++ b/luci-theme-glass/less/responsive.less @@ -0,0 +1,91 @@ +/* ========================================================================== + Responsive — Media queries for tablet & mobile + ========================================================================== */ + +/* Tablet: sidebar overlay */ +@media (max-width: 1024px) { + #sidebar { + transform: translateX(calc(-100% - var(--sidebar-gap))); + + &.open { + transform: translateX(0); + } + } + + #header { + left: 0; + } + + #header-sub { + left: 0; + max-width: calc(100vw - var(--sidebar-gap)); + } + + #content-wrap { + margin-left: 0; + } + + .glass-footer { + margin-left: 0; + } + + .menu-toggle { + display: block; + } + + .sidebar-overlay.active { + display: block; + } +} + +/* Hide status indicators on small screens */ +@media (max-width: 768px) { + #status-indicators { + display: none !important; + } +} + +/* Mobile: tighter spacing */ +@media (max-width: 640px) { + :root { + --content-padding: 16px; + --sidebar-width: 280px; + } + + .cbi-value { + flex-direction: column; + } + + .cbi-value-title { + flex: none; + padding-right: 0; + padding-bottom: var(--space-xs); + } + + .cbi-page-actions { + flex-direction: column; + + .cbi-button { + width: 100%; + } + } + + .login-card { + padding: var(--space-2xl) var(--space-xl); + } + + .cbi-tabmenu { + overflow-x: auto; + flex-wrap: nowrap; + + li a { + white-space: nowrap; + } + } + + table.cbi-section-table, + .table { + display: block; + overflow-x: auto; + } +} diff --git a/luci-theme-glass/less/sysauth.less b/luci-theme-glass/less/sysauth.less new file mode 100644 index 000000000..863bab06d --- /dev/null +++ b/luci-theme-glass/less/sysauth.less @@ -0,0 +1,160 @@ +/* ========================================================================== + Login Page (sysauth) — luci-theme-glass + ========================================================================== */ + +.login-wrap { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + padding: var(--space-xl); +} + +.login-card { + /* True frosted glass — translucent, not white box */ + background-image: linear-gradient( + 160deg, + rgba(255, 255, 255, 0.25) 0%, + rgba(255, 255, 255, 0.20) 25%, + rgba(245, 250, 255, 0.22) 50%, + rgba(255, 255, 255, 0.18) 75%, + rgba(240, 248, 255, 0.21) 100% + ); + background-color: rgba(255, 255, 255, 0.55); + backdrop-filter: blur(30px) saturate(var(--glass-saturate)); + -webkit-backdrop-filter: blur(30px) saturate(var(--glass-saturate)); + border: 0.5px solid rgba(255, 255, 255, 0.35); + box-shadow: + 0 8px 40px rgba(0, 0, 0, 0.10), + 0 2px 8px rgba(0, 0, 0, 0.05), + inset 0 1px 0 rgba(255, 255, 255, 0.50), + inset 0 0 16px rgba(255, 255, 255, 0.15); + border-radius: var(--radius-xl); + padding: var(--space-3xl) var(--space-2xl); + width: 100%; + max-width: 380px; + position: relative; + text-align: center; +} + +.login-logo { + width: 64px; + height: 64px; + margin: 0 auto var(--space-lg); +} + +.login-title { + font-size: var(--font-size-xl); + font-weight: var(--font-weight-bold); + margin-bottom: var(--space-xs); + color: var(--color-text); +} + +.login-subtitle { + font-size: var(--font-size-sm); + color: var(--color-text-secondary); + margin-bottom: var(--space-sm); +} + +.login-version { + font-size: var(--font-size-sm); + color: var(--color-text-tertiary); + margin-bottom: var(--space-2xl); +} + +.login-form { + text-align: left; + + .login-field { + margin-bottom: var(--space-lg); + + label { + display: block; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--color-text-secondary); + margin-bottom: var(--space-xs); + } + + input { + width: 100%; + max-width: none; + padding: var(--space-md) var(--space-lg); + background: rgba(255, 255, 255, 0.30); + border: 0.5px solid rgba(255, 255, 255, 0.35); + border-radius: var(--radius-md); + font-size: var(--font-size-base); + color: var(--color-text); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.25), + inset 0 0 4px rgba(255, 255, 255, 0.08); + transition: border-color var(--transition-fast), box-shadow var(--transition-fast); + + &:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: + 0 0 0 3px rgba(var(--color-primary-rgb), 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.25); + } + + &::placeholder { + color: var(--color-text-tertiary); + } + } + } +} + +.login-btn { + width: 100%; + padding: var(--space-md) var(--space-lg); + /* Glass button — glass-ui .glass-bg pattern */ + background-image: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.15) 0%, + rgba(255, 255, 255, 0.12) 25%, + rgba(240, 248, 255, 0.13) 50%, + rgba(255, 255, 255, 0.11) 75%, + rgba(230, 240, 255, 0.12) 100% + ); + background-color: transparent; + backdrop-filter: blur(var(--glass-blur)) saturate(180%); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(180%); + border: 0.5px solid rgba(255, 255, 255, 0.12); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.08), + 0 1px 4px rgba(0, 0, 0, 0.04), + inset 0 0 0.5px rgba(255, 255, 255, 0.08), + inset 0 0 12px rgba(255, 255, 255, 0.10); + color: var(--color-text); + border-radius: var(--radius-md); + font-size: var(--font-size-base); + font-weight: var(--font-weight-semibold); + cursor: pointer; + transition: all var(--transition-fast); + + &:hover { + opacity: 0.90; + } + + &:active { + transform: scale(0.98); + opacity: 0.80; + box-shadow: + inset 0 2px 4px rgba(0, 0, 0, 0.15), + inset 0 1px 2px rgba(0, 0, 0, 0.10); + } +} + +.login-error { + background: rgba(255, 59, 48, 0.10); + border: 1px solid rgba(255, 59, 48, 0.30); + color: var(--color-danger); + padding: var(--space-md) var(--space-lg); + border-radius: var(--radius-md); + font-size: var(--font-size-sm); + margin-bottom: var(--space-lg); + text-align: center; +} diff --git a/luci-theme-glass/less/variables.less b/luci-theme-glass/less/variables.less new file mode 100644 index 000000000..1494adadc --- /dev/null +++ b/luci-theme-glass/less/variables.less @@ -0,0 +1,99 @@ +/* ========================================================================== + Design Tokens — luci-theme-glass + All CSS custom properties live here for easy theming. + ========================================================================== */ + +:root { + /* ---- Typography ---- */ + --font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", + "Segoe UI", Roboto, "Noto Sans", sans-serif; + --font-mono: "SF Mono", SFMono-Regular, ui-monospace, Menlo, Consolas, + "Liberation Mono", monospace; + --font-size-base: 0.875rem; + --font-size-sm: 0.8125rem; + --font-size-lg: 1rem; + --font-size-xl: 1.25rem; + --font-size-2xl: 1.75rem; + --line-height: 1.5; + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + + /* ---- Colors — Light mode ---- */ + --color-primary: #007AFF; + --color-primary-hover: #0066d6; + --color-primary-active: #004fad; + --color-primary-rgb: 0, 122, 255; + + --color-success: #34C759; + --color-warning: #FF9500; + --color-danger: #FF3B30; + --color-info: #5AC8FA; + + --color-text: #1d1d1f; + --color-text-secondary: #6e6e73; + --color-text-tertiary: #aeaeb2; + --color-text-inverse: #ffffff; + + --color-bg: #f5f5f7; + --color-bg-elevated: #ffffff; + --color-bg-secondary: #e8e8ed; + + --color-border: rgba(0, 0, 0, 0.08); + --color-border-strong: rgba(0, 0, 0, 0.16); + --color-separator: rgba(60, 60, 67, 0.12); + + /* ---- Glassmorphism ---- */ + --glass-blur: 20px; + --glass-saturate: 180%; + --glass-bg: rgba(255, 255, 255, 0.72); + --glass-bg-hover: rgba(255, 255, 255, 0.82); + --glass-bg-strong: rgba(255, 255, 255, 0.85); + --glass-border: rgba(255, 255, 255, 0.45); + --glass-shadow: 0 2px 16px rgba(0, 0, 0, 0.06), + 0 0 1px rgba(0, 0, 0, 0.08); + --glass-shadow-lg: 0 8px 40px rgba(0, 0, 0, 0.10), + 0 0 1px rgba(0, 0, 0, 0.06); + + /* ---- Layout ---- */ + --sidebar-width: 260px; + --sidebar-collapsed-width: 64px; + --sidebar-gap: 10px; + --header-height: 56px; + --content-max-width: none; + --content-padding: 24px; + + /* ---- Radius ---- */ + --radius-sm: 6px; + --radius-md: 10px; + --radius-lg: 14px; + --radius-xl: 20px; + --radius-full: 9999px; + + /* ---- Spacing ---- */ + --space-xs: 4px; + --space-sm: 8px; + --space-md: 12px; + --space-lg: 16px; + --space-xl: 24px; + --space-2xl: 32px; + --space-3xl: 48px; + + /* ---- Transitions ---- */ + --transition-fast: 0.15s ease; + --transition-normal: 0.25s ease; + --transition-slow: 0.4s cubic-bezier(0.16, 1, 0.3, 1); + + /* ---- Z-index ---- */ + --z-dropdown: 100; + --z-sticky: 200; + --z-sidebar: 300; + --z-header: 400; + --z-overlay: 500; + --z-modal: 600; + --z-toast: 700; + + /* ---- Background ---- */ + --bg-gradient: linear-gradient(135deg, #b8c4d0 0%, #d5dce5 50%, #ddd5cc 100%); +} diff --git a/luci-theme-glass/po/de/glass.po b/luci-theme-glass/po/de/glass.po new file mode 100644 index 000000000..78edcfd7d --- /dev/null +++ b/luci-theme-glass/po/de/glass.po @@ -0,0 +1,209 @@ +msgid "" +msgstr "" +"Language: de\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: htdocs/luci-static/resources/view/system/glass.js:52 +msgid "Accent Colors" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:72 +msgid "Accent color used for active elements, links, and buttons in dark mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:56 +msgid "" +"Accent color used for active elements, links, and buttons in light mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:128 +msgid "Active" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:33 +msgid "Auto (follow system)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:108 +msgid "Backdrop blur radius for glass panels in dark mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:92 +msgid "" +"Backdrop blur radius for glass panels. Higher values create a more frosted " +"appearance." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:120 +msgid "Background" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:98 +msgid "" +"Background opacity of glass panels (0 = fully transparent, 1 = fully opaque)." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:114 +msgid "" +"Background opacity of glass panels in dark mode (0 = fully transparent, 1 = " +"fully opaque)." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:43 +msgid "Base font size" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:91 +#: htdocs/luci-static/resources/view/system/glass.js:107 +msgid "Blur intensity (px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:25 +msgid "" +"Configure the appearance of the Glass theme. Changes take effect after " +"saving and refreshing the page." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:32 +msgid "" +"Controls the overall appearance. \"Normal\" follows the system preference." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:123 +msgid "Current background" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:35 +msgid "Dark" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:48 +msgid "Extra Large (18px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:28 +msgid "General" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:104 +msgid "Glass Effects — Dark Mode" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:88 +msgid "Glass Effects — Light Mode" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:24 +msgid "Glass Theme" +msgstr "" + +#: root/usr/share/luci/menu.d/luci-theme-glass.json:3 +msgid "Glass Theme Config" +msgstr "" + +#: ucode/template/themes/glass/header.ut:204 +msgid "Go to password configuration..." +msgstr "" + +#: root/usr/share/rpcd/acl.d/luci-theme-glass.json:3 +msgid "Grant access for luci-theme-glass" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:38 +msgid "Header status bar" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:111 +msgid "Invalid username and/or password! Please try again." +msgstr "" + +#: ucode/template/themes/glass/header.ut:211 +msgid "JavaScript required!" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:47 +msgid "Large (16px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:34 +msgid "Light" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:129 +msgid "Log in" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:51 +msgid "Login" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:131 +msgid "" +"No background set. Upload an image or video named \"bg\" (e.g. bg.jpg, " +"bg.png, bg.mp4) to /www/luci-static/glass/background/ via SCP." +msgstr "" + +#: ucode/template/themes/glass/header.ut:201 +msgid "No password set!" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:46 +msgid "Normal (14px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:97 +#: htdocs/luci-static/resources/view/system/glass.js:113 +msgid "Panel transparency" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:124 +msgid "Password" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:71 +msgid "Primary color (dark mode)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:55 +msgid "Primary color (light mode)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:44 +msgid "" +"Scales all text in the theme. Larger values improve readability on high-DPI " +"displays." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:39 +msgid "" +"Show live system stats (CPU, RAM, network, uptime) in the header. Disable to " +"reduce resource usage on low-end devices." +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:106 +msgid "Sign in to continue" +msgstr "Zum Fortfahren anmelden" + +#: htdocs/luci-static/resources/view/system/glass.js:45 +msgid "Small (13px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:31 +msgid "Theme mode" +msgstr "" + +#: ucode/template/themes/glass/header.ut:202 +msgid "" +"There is no password set on this router. Please configure a root password to " +"protect the web interface." +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:117 +msgid "Username" +msgstr "" + +#: ucode/template/themes/glass/header.ut:212 +msgid "" +"You must enable JavaScript in your browser or LuCI will not work properly." +msgstr "" diff --git a/luci-theme-glass/po/es/glass.po b/luci-theme-glass/po/es/glass.po new file mode 100644 index 000000000..a4874c317 --- /dev/null +++ b/luci-theme-glass/po/es/glass.po @@ -0,0 +1,242 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Poedit 3.8\n" + +#: htdocs/luci-static/resources/view/system/glass.js:52 +msgid "Accent Colors" +msgstr "Colores de acento" + +#: htdocs/luci-static/resources/view/system/glass.js:72 +msgid "Accent color used for active elements, links, and buttons in dark mode." +msgstr "" +"Color de acento utilizado para elementos activos, enlaces y botones en el " +"modo oscuro." + +#: htdocs/luci-static/resources/view/system/glass.js:56 +msgid "" +"Accent color used for active elements, links, and buttons in light mode." +msgstr "" +"Color de acento utilizado para elementos activos, enlaces y botones en el " +"modo claro." + +#: htdocs/luci-static/resources/view/system/glass.js:128 +msgid "Active" +msgstr "Activo" + +#: htdocs/luci-static/resources/view/system/glass.js:33 +msgid "Auto (follow system)" +msgstr "Automático (seguir el del sistema)" + +#: htdocs/luci-static/resources/view/system/glass.js:108 +msgid "Backdrop blur radius for glass panels in dark mode." +msgstr "Radio de desenfoque del fondo para paneles de cristal en modo oscuro." + +#: htdocs/luci-static/resources/view/system/glass.js:92 +msgid "" +"Backdrop blur radius for glass panels. Higher values create a more frosted " +"appearance." +msgstr "" +"Radio de desenfoque del fondo para paneles de cristal. Los valores más altos " +"crean una apariencia más esmerilada." + +#: htdocs/luci-static/resources/view/system/glass.js:120 +msgid "Background" +msgstr "Fondo" + +#: htdocs/luci-static/resources/view/system/glass.js:98 +msgid "" +"Background opacity of glass panels (0 = fully transparent, 1 = fully opaque)." +msgstr "" +"Opacidad de fondo de los paneles de cristal (0 = totalmente transparente, 1 " +"= totalmente opaco)." + +#: htdocs/luci-static/resources/view/system/glass.js:114 +msgid "" +"Background opacity of glass panels in dark mode (0 = fully transparent, 1 = " +"fully opaque)." +msgstr "" +"Opacidad de fondo de los paneles de cristal en modo oscuro (0 = totalmente " +"transparente, 1 = totalmente opaco)." + +#: htdocs/luci-static/resources/view/system/glass.js:43 +msgid "Base font size" +msgstr "Tamaño de fuente base" + +#: htdocs/luci-static/resources/view/system/glass.js:91 +#: htdocs/luci-static/resources/view/system/glass.js:107 +msgid "Blur intensity (px)" +msgstr "Intensidad del desenfoque (px)" + +#: htdocs/luci-static/resources/view/system/glass.js:25 +msgid "" +"Configure the appearance of the Glass theme. Changes take effect after " +"saving and refreshing the page." +msgstr "" +"Configure la apariencia del tema Glass. Los cambios surtirán efecto después " +"de guardar y actualizar la página." + +#: htdocs/luci-static/resources/view/system/glass.js:32 +msgid "" +"Controls the overall appearance. \"Normal\" follows the system preference." +msgstr "" +"Controla la apariencia general. La opción «Normal» respeta la preferencia " +"del sistema." + +#: htdocs/luci-static/resources/view/system/glass.js:123 +msgid "Current background" +msgstr "Fondo actual" + +#: htdocs/luci-static/resources/view/system/glass.js:35 +msgid "Dark" +msgstr "Oscuro" + +#: htdocs/luci-static/resources/view/system/glass.js:48 +msgid "Extra Large (18px)" +msgstr "Extra grande (18px)" + +#: htdocs/luci-static/resources/view/system/glass.js:28 +msgid "General" +msgstr "General" + +#: htdocs/luci-static/resources/view/system/glass.js:104 +msgid "Glass Effects — Dark Mode" +msgstr "Efectos de vidrio - Modo oscuro" + +#: htdocs/luci-static/resources/view/system/glass.js:88 +msgid "Glass Effects — Light Mode" +msgstr "Efectos de vidrio - Modo claro" + +#: htdocs/luci-static/resources/view/system/glass.js:24 +msgid "Glass Theme" +msgstr "Tema Glass" + +#: root/usr/share/luci/menu.d/luci-theme-glass.json:3 +msgid "Glass Theme Config" +msgstr "Configuración de Glass" + +#: ucode/template/themes/glass/header.ut:204 +msgid "Go to password configuration..." +msgstr "Ir a la configuración de contraseñas..." + +#: root/usr/share/rpcd/acl.d/luci-theme-glass.json:3 +msgid "Grant access for luci-theme-glass" +msgstr "Conceder acceso para luci-theme-glass" + +#: htdocs/luci-static/resources/view/system/glass.js:38 +msgid "Header status bar" +msgstr "Barra de estado del encabezado" + +#: ucode/template/themes/glass/sysauth.ut:111 +msgid "Invalid username and/or password! Please try again." +msgstr "Nombre de usuario y/o contraseña incorrectos. Inténtelo de nuevo." + +#: ucode/template/themes/glass/header.ut:211 +msgid "JavaScript required!" +msgstr "¡Se requiere JavaScript!" + +#: htdocs/luci-static/resources/view/system/glass.js:47 +msgid "Large (16px)" +msgstr "Grande (16px)" + +#: htdocs/luci-static/resources/view/system/glass.js:34 +msgid "Light" +msgstr "Claro" + +#: ucode/template/themes/glass/sysauth.ut:129 +msgid "Log in" +msgstr "Iniciar sesión" + +#: ucode/template/themes/glass/sysauth.ut:51 +msgid "Login" +msgstr "Acceso" + +#: htdocs/luci-static/resources/view/system/glass.js:131 +msgid "" +"No background set. Upload an image or video named \"bg\" (e.g. bg.jpg, " +"bg.png, bg.mp4) to /www/luci-static/glass/background/ via SCP." +msgstr "" +"No se ha configurado ningún fondo. Cargue una imagen o video llamado \"bg\" " +"(por ejemplo, bg.jpg, bg.png, bg.mp4) a /www/luci-static/glass/background/ a " +"través de SCP." + +#: ucode/template/themes/glass/header.ut:201 +msgid "No password set!" +msgstr "¡No se ha establecido ninguna contraseña!" + +#: htdocs/luci-static/resources/view/system/glass.js:46 +msgid "Normal (14px)" +msgstr "Normal (14px)" + +#: htdocs/luci-static/resources/view/system/glass.js:97 +#: htdocs/luci-static/resources/view/system/glass.js:113 +msgid "Panel transparency" +msgstr "Transparencia del panel" + +#: ucode/template/themes/glass/sysauth.ut:124 +msgid "Password" +msgstr "Contraseña" + +#: htdocs/luci-static/resources/view/system/glass.js:71 +msgid "Primary color (dark mode)" +msgstr "Color principal (modo oscuro)" + +#: htdocs/luci-static/resources/view/system/glass.js:55 +msgid "Primary color (light mode)" +msgstr "Color principal (modo claro)" + +#: htdocs/luci-static/resources/view/system/glass.js:44 +msgid "" +"Scales all text in the theme. Larger values improve readability on high-DPI " +"displays." +msgstr "" +"Ajusta el tamaño de todo el texto del tema. Los valores mayores mejoran la " +"legibilidad en pantallas de alta resolución." + +#: htdocs/luci-static/resources/view/system/glass.js:39 +msgid "" +"Show live system stats (CPU, RAM, network, uptime) in the header. Disable to " +"reduce resource usage on low-end devices." +msgstr "" +"Muestra estadísticas del sistema en tiempo real (CPU, RAM, red, tiempo de " +"actividad) en el encabezado. Desactive para reducir el consumo de recursos " +"en dispositivos de gama baja." + +#: ucode/template/themes/glass/sysauth.ut:106 +msgid "Sign in to continue" +msgstr "Inicia sesión para continuar" + +#: htdocs/luci-static/resources/view/system/glass.js:45 +msgid "Small (13px)" +msgstr "Pequeño (13px)" + +#: htdocs/luci-static/resources/view/system/glass.js:31 +msgid "Theme mode" +msgstr "Modo del tema" + +#: ucode/template/themes/glass/header.ut:202 +msgid "" +"There is no password set on this router. Please configure a root password to " +"protect the web interface." +msgstr "" +"Este enrutador no tiene contraseña. Configure una contraseña de " +"administrador para proteger la interfaz web." + +#: ucode/template/themes/glass/sysauth.ut:117 +msgid "Username" +msgstr "Nombre de usuario" + +#: ucode/template/themes/glass/header.ut:212 +msgid "" +"You must enable JavaScript in your browser or LuCI will not work properly." +msgstr "" +"Debe activar JavaScript en su navegador o LuCI no funcionará correctamente." diff --git a/luci-theme-glass/po/fr/glass.po b/luci-theme-glass/po/fr/glass.po new file mode 100644 index 000000000..8ad834e62 --- /dev/null +++ b/luci-theme-glass/po/fr/glass.po @@ -0,0 +1,209 @@ +msgid "" +msgstr "" +"Language: fr\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: htdocs/luci-static/resources/view/system/glass.js:52 +msgid "Accent Colors" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:72 +msgid "Accent color used for active elements, links, and buttons in dark mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:56 +msgid "" +"Accent color used for active elements, links, and buttons in light mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:128 +msgid "Active" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:33 +msgid "Auto (follow system)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:108 +msgid "Backdrop blur radius for glass panels in dark mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:92 +msgid "" +"Backdrop blur radius for glass panels. Higher values create a more frosted " +"appearance." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:120 +msgid "Background" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:98 +msgid "" +"Background opacity of glass panels (0 = fully transparent, 1 = fully opaque)." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:114 +msgid "" +"Background opacity of glass panels in dark mode (0 = fully transparent, 1 = " +"fully opaque)." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:43 +msgid "Base font size" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:91 +#: htdocs/luci-static/resources/view/system/glass.js:107 +msgid "Blur intensity (px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:25 +msgid "" +"Configure the appearance of the Glass theme. Changes take effect after " +"saving and refreshing the page." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:32 +msgid "" +"Controls the overall appearance. \"Normal\" follows the system preference." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:123 +msgid "Current background" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:35 +msgid "Dark" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:48 +msgid "Extra Large (18px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:28 +msgid "General" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:104 +msgid "Glass Effects — Dark Mode" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:88 +msgid "Glass Effects — Light Mode" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:24 +msgid "Glass Theme" +msgstr "" + +#: root/usr/share/luci/menu.d/luci-theme-glass.json:3 +msgid "Glass Theme Config" +msgstr "" + +#: ucode/template/themes/glass/header.ut:204 +msgid "Go to password configuration..." +msgstr "" + +#: root/usr/share/rpcd/acl.d/luci-theme-glass.json:3 +msgid "Grant access for luci-theme-glass" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:38 +msgid "Header status bar" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:111 +msgid "Invalid username and/or password! Please try again." +msgstr "" + +#: ucode/template/themes/glass/header.ut:211 +msgid "JavaScript required!" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:47 +msgid "Large (16px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:34 +msgid "Light" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:129 +msgid "Log in" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:51 +msgid "Login" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:131 +msgid "" +"No background set. Upload an image or video named \"bg\" (e.g. bg.jpg, " +"bg.png, bg.mp4) to /www/luci-static/glass/background/ via SCP." +msgstr "" + +#: ucode/template/themes/glass/header.ut:201 +msgid "No password set!" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:46 +msgid "Normal (14px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:97 +#: htdocs/luci-static/resources/view/system/glass.js:113 +msgid "Panel transparency" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:124 +msgid "Password" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:71 +msgid "Primary color (dark mode)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:55 +msgid "Primary color (light mode)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:44 +msgid "" +"Scales all text in the theme. Larger values improve readability on high-DPI " +"displays." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:39 +msgid "" +"Show live system stats (CPU, RAM, network, uptime) in the header. Disable to " +"reduce resource usage on low-end devices." +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:106 +msgid "Sign in to continue" +msgstr "Connectez-vous pour continuer" + +#: htdocs/luci-static/resources/view/system/glass.js:45 +msgid "Small (13px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:31 +msgid "Theme mode" +msgstr "" + +#: ucode/template/themes/glass/header.ut:202 +msgid "" +"There is no password set on this router. Please configure a root password to " +"protect the web interface." +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:117 +msgid "Username" +msgstr "" + +#: ucode/template/themes/glass/header.ut:212 +msgid "" +"You must enable JavaScript in your browser or LuCI will not work properly." +msgstr "" diff --git a/luci-theme-glass/po/ja/glass.po b/luci-theme-glass/po/ja/glass.po new file mode 100644 index 000000000..663e75242 --- /dev/null +++ b/luci-theme-glass/po/ja/glass.po @@ -0,0 +1,209 @@ +msgid "" +msgstr "" +"Language: ja\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: htdocs/luci-static/resources/view/system/glass.js:52 +msgid "Accent Colors" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:72 +msgid "Accent color used for active elements, links, and buttons in dark mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:56 +msgid "" +"Accent color used for active elements, links, and buttons in light mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:128 +msgid "Active" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:33 +msgid "Auto (follow system)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:108 +msgid "Backdrop blur radius for glass panels in dark mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:92 +msgid "" +"Backdrop blur radius for glass panels. Higher values create a more frosted " +"appearance." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:120 +msgid "Background" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:98 +msgid "" +"Background opacity of glass panels (0 = fully transparent, 1 = fully opaque)." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:114 +msgid "" +"Background opacity of glass panels in dark mode (0 = fully transparent, 1 = " +"fully opaque)." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:43 +msgid "Base font size" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:91 +#: htdocs/luci-static/resources/view/system/glass.js:107 +msgid "Blur intensity (px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:25 +msgid "" +"Configure the appearance of the Glass theme. Changes take effect after " +"saving and refreshing the page." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:32 +msgid "" +"Controls the overall appearance. \"Normal\" follows the system preference." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:123 +msgid "Current background" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:35 +msgid "Dark" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:48 +msgid "Extra Large (18px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:28 +msgid "General" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:104 +msgid "Glass Effects — Dark Mode" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:88 +msgid "Glass Effects — Light Mode" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:24 +msgid "Glass Theme" +msgstr "" + +#: root/usr/share/luci/menu.d/luci-theme-glass.json:3 +msgid "Glass Theme Config" +msgstr "" + +#: ucode/template/themes/glass/header.ut:204 +msgid "Go to password configuration..." +msgstr "" + +#: root/usr/share/rpcd/acl.d/luci-theme-glass.json:3 +msgid "Grant access for luci-theme-glass" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:38 +msgid "Header status bar" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:111 +msgid "Invalid username and/or password! Please try again." +msgstr "" + +#: ucode/template/themes/glass/header.ut:211 +msgid "JavaScript required!" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:47 +msgid "Large (16px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:34 +msgid "Light" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:129 +msgid "Log in" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:51 +msgid "Login" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:131 +msgid "" +"No background set. Upload an image or video named \"bg\" (e.g. bg.jpg, " +"bg.png, bg.mp4) to /www/luci-static/glass/background/ via SCP." +msgstr "" + +#: ucode/template/themes/glass/header.ut:201 +msgid "No password set!" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:46 +msgid "Normal (14px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:97 +#: htdocs/luci-static/resources/view/system/glass.js:113 +msgid "Panel transparency" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:124 +msgid "Password" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:71 +msgid "Primary color (dark mode)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:55 +msgid "Primary color (light mode)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:44 +msgid "" +"Scales all text in the theme. Larger values improve readability on high-DPI " +"displays." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:39 +msgid "" +"Show live system stats (CPU, RAM, network, uptime) in the header. Disable to " +"reduce resource usage on low-end devices." +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:106 +msgid "Sign in to continue" +msgstr "続行するにはサインインしてください" + +#: htdocs/luci-static/resources/view/system/glass.js:45 +msgid "Small (13px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:31 +msgid "Theme mode" +msgstr "" + +#: ucode/template/themes/glass/header.ut:202 +msgid "" +"There is no password set on this router. Please configure a root password to " +"protect the web interface." +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:117 +msgid "Username" +msgstr "" + +#: ucode/template/themes/glass/header.ut:212 +msgid "" +"You must enable JavaScript in your browser or LuCI will not work properly." +msgstr "" diff --git a/luci-theme-glass/po/ko/glass.po b/luci-theme-glass/po/ko/glass.po new file mode 100644 index 000000000..1c398ddd1 --- /dev/null +++ b/luci-theme-glass/po/ko/glass.po @@ -0,0 +1,209 @@ +msgid "" +msgstr "" +"Language: ko\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: htdocs/luci-static/resources/view/system/glass.js:52 +msgid "Accent Colors" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:72 +msgid "Accent color used for active elements, links, and buttons in dark mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:56 +msgid "" +"Accent color used for active elements, links, and buttons in light mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:128 +msgid "Active" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:33 +msgid "Auto (follow system)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:108 +msgid "Backdrop blur radius for glass panels in dark mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:92 +msgid "" +"Backdrop blur radius for glass panels. Higher values create a more frosted " +"appearance." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:120 +msgid "Background" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:98 +msgid "" +"Background opacity of glass panels (0 = fully transparent, 1 = fully opaque)." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:114 +msgid "" +"Background opacity of glass panels in dark mode (0 = fully transparent, 1 = " +"fully opaque)." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:43 +msgid "Base font size" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:91 +#: htdocs/luci-static/resources/view/system/glass.js:107 +msgid "Blur intensity (px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:25 +msgid "" +"Configure the appearance of the Glass theme. Changes take effect after " +"saving and refreshing the page." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:32 +msgid "" +"Controls the overall appearance. \"Normal\" follows the system preference." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:123 +msgid "Current background" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:35 +msgid "Dark" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:48 +msgid "Extra Large (18px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:28 +msgid "General" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:104 +msgid "Glass Effects — Dark Mode" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:88 +msgid "Glass Effects — Light Mode" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:24 +msgid "Glass Theme" +msgstr "" + +#: root/usr/share/luci/menu.d/luci-theme-glass.json:3 +msgid "Glass Theme Config" +msgstr "" + +#: ucode/template/themes/glass/header.ut:204 +msgid "Go to password configuration..." +msgstr "" + +#: root/usr/share/rpcd/acl.d/luci-theme-glass.json:3 +msgid "Grant access for luci-theme-glass" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:38 +msgid "Header status bar" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:111 +msgid "Invalid username and/or password! Please try again." +msgstr "" + +#: ucode/template/themes/glass/header.ut:211 +msgid "JavaScript required!" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:47 +msgid "Large (16px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:34 +msgid "Light" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:129 +msgid "Log in" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:51 +msgid "Login" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:131 +msgid "" +"No background set. Upload an image or video named \"bg\" (e.g. bg.jpg, " +"bg.png, bg.mp4) to /www/luci-static/glass/background/ via SCP." +msgstr "" + +#: ucode/template/themes/glass/header.ut:201 +msgid "No password set!" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:46 +msgid "Normal (14px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:97 +#: htdocs/luci-static/resources/view/system/glass.js:113 +msgid "Panel transparency" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:124 +msgid "Password" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:71 +msgid "Primary color (dark mode)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:55 +msgid "Primary color (light mode)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:44 +msgid "" +"Scales all text in the theme. Larger values improve readability on high-DPI " +"displays." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:39 +msgid "" +"Show live system stats (CPU, RAM, network, uptime) in the header. Disable to " +"reduce resource usage on low-end devices." +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:106 +msgid "Sign in to continue" +msgstr "계속하려면 로그인하세요" + +#: htdocs/luci-static/resources/view/system/glass.js:45 +msgid "Small (13px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:31 +msgid "Theme mode" +msgstr "" + +#: ucode/template/themes/glass/header.ut:202 +msgid "" +"There is no password set on this router. Please configure a root password to " +"protect the web interface." +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:117 +msgid "Username" +msgstr "" + +#: ucode/template/themes/glass/header.ut:212 +msgid "" +"You must enable JavaScript in your browser or LuCI will not work properly." +msgstr "" diff --git a/luci-theme-glass/po/pt-br/glass.po b/luci-theme-glass/po/pt-br/glass.po new file mode 100644 index 000000000..85681ecf7 --- /dev/null +++ b/luci-theme-glass/po/pt-br/glass.po @@ -0,0 +1,209 @@ +msgid "" +msgstr "" +"Language: pt_BR\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: htdocs/luci-static/resources/view/system/glass.js:52 +msgid "Accent Colors" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:72 +msgid "Accent color used for active elements, links, and buttons in dark mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:56 +msgid "" +"Accent color used for active elements, links, and buttons in light mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:128 +msgid "Active" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:33 +msgid "Auto (follow system)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:108 +msgid "Backdrop blur radius for glass panels in dark mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:92 +msgid "" +"Backdrop blur radius for glass panels. Higher values create a more frosted " +"appearance." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:120 +msgid "Background" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:98 +msgid "" +"Background opacity of glass panels (0 = fully transparent, 1 = fully opaque)." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:114 +msgid "" +"Background opacity of glass panels in dark mode (0 = fully transparent, 1 = " +"fully opaque)." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:43 +msgid "Base font size" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:91 +#: htdocs/luci-static/resources/view/system/glass.js:107 +msgid "Blur intensity (px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:25 +msgid "" +"Configure the appearance of the Glass theme. Changes take effect after " +"saving and refreshing the page." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:32 +msgid "" +"Controls the overall appearance. \"Normal\" follows the system preference." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:123 +msgid "Current background" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:35 +msgid "Dark" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:48 +msgid "Extra Large (18px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:28 +msgid "General" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:104 +msgid "Glass Effects — Dark Mode" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:88 +msgid "Glass Effects — Light Mode" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:24 +msgid "Glass Theme" +msgstr "" + +#: root/usr/share/luci/menu.d/luci-theme-glass.json:3 +msgid "Glass Theme Config" +msgstr "" + +#: ucode/template/themes/glass/header.ut:204 +msgid "Go to password configuration..." +msgstr "" + +#: root/usr/share/rpcd/acl.d/luci-theme-glass.json:3 +msgid "Grant access for luci-theme-glass" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:38 +msgid "Header status bar" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:111 +msgid "Invalid username and/or password! Please try again." +msgstr "" + +#: ucode/template/themes/glass/header.ut:211 +msgid "JavaScript required!" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:47 +msgid "Large (16px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:34 +msgid "Light" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:129 +msgid "Log in" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:51 +msgid "Login" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:131 +msgid "" +"No background set. Upload an image or video named \"bg\" (e.g. bg.jpg, " +"bg.png, bg.mp4) to /www/luci-static/glass/background/ via SCP." +msgstr "" + +#: ucode/template/themes/glass/header.ut:201 +msgid "No password set!" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:46 +msgid "Normal (14px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:97 +#: htdocs/luci-static/resources/view/system/glass.js:113 +msgid "Panel transparency" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:124 +msgid "Password" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:71 +msgid "Primary color (dark mode)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:55 +msgid "Primary color (light mode)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:44 +msgid "" +"Scales all text in the theme. Larger values improve readability on high-DPI " +"displays." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:39 +msgid "" +"Show live system stats (CPU, RAM, network, uptime) in the header. Disable to " +"reduce resource usage on low-end devices." +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:106 +msgid "Sign in to continue" +msgstr "Faça login para continuar" + +#: htdocs/luci-static/resources/view/system/glass.js:45 +msgid "Small (13px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:31 +msgid "Theme mode" +msgstr "" + +#: ucode/template/themes/glass/header.ut:202 +msgid "" +"There is no password set on this router. Please configure a root password to " +"protect the web interface." +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:117 +msgid "Username" +msgstr "" + +#: ucode/template/themes/glass/header.ut:212 +msgid "" +"You must enable JavaScript in your browser or LuCI will not work properly." +msgstr "" diff --git a/luci-theme-glass/po/ru/glass.po b/luci-theme-glass/po/ru/glass.po new file mode 100644 index 000000000..cf2eda5e6 --- /dev/null +++ b/luci-theme-glass/po/ru/glass.po @@ -0,0 +1,209 @@ +msgid "" +msgstr "" +"Language: ru\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" + +#: htdocs/luci-static/resources/view/system/glass.js:52 +msgid "Accent Colors" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:72 +msgid "Accent color used for active elements, links, and buttons in dark mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:56 +msgid "" +"Accent color used for active elements, links, and buttons in light mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:128 +msgid "Active" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:33 +msgid "Auto (follow system)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:108 +msgid "Backdrop blur radius for glass panels in dark mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:92 +msgid "" +"Backdrop blur radius for glass panels. Higher values create a more frosted " +"appearance." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:120 +msgid "Background" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:98 +msgid "" +"Background opacity of glass panels (0 = fully transparent, 1 = fully opaque)." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:114 +msgid "" +"Background opacity of glass panels in dark mode (0 = fully transparent, 1 = " +"fully opaque)." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:43 +msgid "Base font size" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:91 +#: htdocs/luci-static/resources/view/system/glass.js:107 +msgid "Blur intensity (px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:25 +msgid "" +"Configure the appearance of the Glass theme. Changes take effect after " +"saving and refreshing the page." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:32 +msgid "" +"Controls the overall appearance. \"Normal\" follows the system preference." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:123 +msgid "Current background" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:35 +msgid "Dark" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:48 +msgid "Extra Large (18px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:28 +msgid "General" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:104 +msgid "Glass Effects — Dark Mode" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:88 +msgid "Glass Effects — Light Mode" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:24 +msgid "Glass Theme" +msgstr "" + +#: root/usr/share/luci/menu.d/luci-theme-glass.json:3 +msgid "Glass Theme Config" +msgstr "" + +#: ucode/template/themes/glass/header.ut:204 +msgid "Go to password configuration..." +msgstr "" + +#: root/usr/share/rpcd/acl.d/luci-theme-glass.json:3 +msgid "Grant access for luci-theme-glass" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:38 +msgid "Header status bar" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:111 +msgid "Invalid username and/or password! Please try again." +msgstr "" + +#: ucode/template/themes/glass/header.ut:211 +msgid "JavaScript required!" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:47 +msgid "Large (16px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:34 +msgid "Light" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:129 +msgid "Log in" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:51 +msgid "Login" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:131 +msgid "" +"No background set. Upload an image or video named \"bg\" (e.g. bg.jpg, " +"bg.png, bg.mp4) to /www/luci-static/glass/background/ via SCP." +msgstr "" + +#: ucode/template/themes/glass/header.ut:201 +msgid "No password set!" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:46 +msgid "Normal (14px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:97 +#: htdocs/luci-static/resources/view/system/glass.js:113 +msgid "Panel transparency" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:124 +msgid "Password" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:71 +msgid "Primary color (dark mode)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:55 +msgid "Primary color (light mode)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:44 +msgid "" +"Scales all text in the theme. Larger values improve readability on high-DPI " +"displays." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:39 +msgid "" +"Show live system stats (CPU, RAM, network, uptime) in the header. Disable to " +"reduce resource usage on low-end devices." +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:106 +msgid "Sign in to continue" +msgstr "Войдите для продолжения" + +#: htdocs/luci-static/resources/view/system/glass.js:45 +msgid "Small (13px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:31 +msgid "Theme mode" +msgstr "" + +#: ucode/template/themes/glass/header.ut:202 +msgid "" +"There is no password set on this router. Please configure a root password to " +"protect the web interface." +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:117 +msgid "Username" +msgstr "" + +#: ucode/template/themes/glass/header.ut:212 +msgid "" +"You must enable JavaScript in your browser or LuCI will not work properly." +msgstr "" diff --git a/luci-theme-glass/po/templates/glass.pot b/luci-theme-glass/po/templates/glass.pot new file mode 100644 index 000000000..f39644ba0 --- /dev/null +++ b/luci-theme-glass/po/templates/glass.pot @@ -0,0 +1,206 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: htdocs/luci-static/resources/view/system/glass.js:52 +msgid "Accent Colors" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:72 +msgid "Accent color used for active elements, links, and buttons in dark mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:56 +msgid "" +"Accent color used for active elements, links, and buttons in light mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:128 +msgid "Active" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:33 +msgid "Auto (follow system)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:108 +msgid "Backdrop blur radius for glass panels in dark mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:92 +msgid "" +"Backdrop blur radius for glass panels. Higher values create a more frosted " +"appearance." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:120 +msgid "Background" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:98 +msgid "" +"Background opacity of glass panels (0 = fully transparent, 1 = fully opaque)." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:114 +msgid "" +"Background opacity of glass panels in dark mode (0 = fully transparent, 1 = " +"fully opaque)." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:43 +msgid "Base font size" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:91 +#: htdocs/luci-static/resources/view/system/glass.js:107 +msgid "Blur intensity (px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:25 +msgid "" +"Configure the appearance of the Glass theme. Changes take effect after " +"saving and refreshing the page." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:32 +msgid "" +"Controls the overall appearance. \"Normal\" follows the system preference." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:123 +msgid "Current background" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:35 +msgid "Dark" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:48 +msgid "Extra Large (18px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:28 +msgid "General" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:104 +msgid "Glass Effects — Dark Mode" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:88 +msgid "Glass Effects — Light Mode" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:24 +msgid "Glass Theme" +msgstr "" + +#: root/usr/share/luci/menu.d/luci-theme-glass.json:3 +msgid "Glass Theme Config" +msgstr "" + +#: ucode/template/themes/glass/header.ut:204 +msgid "Go to password configuration..." +msgstr "" + +#: root/usr/share/rpcd/acl.d/luci-theme-glass.json:3 +msgid "Grant access for luci-theme-glass" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:38 +msgid "Header status bar" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:111 +msgid "Invalid username and/or password! Please try again." +msgstr "" + +#: ucode/template/themes/glass/header.ut:211 +msgid "JavaScript required!" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:47 +msgid "Large (16px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:34 +msgid "Light" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:129 +msgid "Log in" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:51 +msgid "Login" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:131 +msgid "" +"No background set. Upload an image or video named \"bg\" (e.g. bg.jpg, " +"bg.png, bg.mp4) to /www/luci-static/glass/background/ via SCP." +msgstr "" + +#: ucode/template/themes/glass/header.ut:201 +msgid "No password set!" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:46 +msgid "Normal (14px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:97 +#: htdocs/luci-static/resources/view/system/glass.js:113 +msgid "Panel transparency" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:124 +msgid "Password" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:71 +msgid "Primary color (dark mode)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:55 +msgid "Primary color (light mode)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:44 +msgid "" +"Scales all text in the theme. Larger values improve readability on high-DPI " +"displays." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:39 +msgid "" +"Show live system stats (CPU, RAM, network, uptime) in the header. Disable to " +"reduce resource usage on low-end devices." +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:106 +msgid "Sign in to continue" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:45 +msgid "Small (13px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:31 +msgid "Theme mode" +msgstr "" + +#: ucode/template/themes/glass/header.ut:202 +msgid "" +"There is no password set on this router. Please configure a root password to " +"protect the web interface." +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:117 +msgid "Username" +msgstr "" + +#: ucode/template/themes/glass/header.ut:212 +msgid "" +"You must enable JavaScript in your browser or LuCI will not work properly." +msgstr "" diff --git a/luci-theme-glass/po/zh-cn b/luci-theme-glass/po/zh-cn new file mode 120000 index 000000000..8d69574dd --- /dev/null +++ b/luci-theme-glass/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-theme-glass/po/zh_Hans/glass.po b/luci-theme-glass/po/zh_Hans/glass.po new file mode 100644 index 000000000..7613aeadc --- /dev/null +++ b/luci-theme-glass/po/zh_Hans/glass.po @@ -0,0 +1,209 @@ +msgid "" +msgstr "" +"Language: zh_Hans\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: htdocs/luci-static/resources/view/system/glass.js:52 +msgid "Accent Colors" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:72 +msgid "Accent color used for active elements, links, and buttons in dark mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:56 +msgid "" +"Accent color used for active elements, links, and buttons in light mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:128 +msgid "Active" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:33 +msgid "Auto (follow system)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:108 +msgid "Backdrop blur radius for glass panels in dark mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:92 +msgid "" +"Backdrop blur radius for glass panels. Higher values create a more frosted " +"appearance." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:120 +msgid "Background" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:98 +msgid "" +"Background opacity of glass panels (0 = fully transparent, 1 = fully opaque)." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:114 +msgid "" +"Background opacity of glass panels in dark mode (0 = fully transparent, 1 = " +"fully opaque)." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:43 +msgid "Base font size" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:91 +#: htdocs/luci-static/resources/view/system/glass.js:107 +msgid "Blur intensity (px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:25 +msgid "" +"Configure the appearance of the Glass theme. Changes take effect after " +"saving and refreshing the page." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:32 +msgid "" +"Controls the overall appearance. \"Normal\" follows the system preference." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:123 +msgid "Current background" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:35 +msgid "Dark" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:48 +msgid "Extra Large (18px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:28 +msgid "General" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:104 +msgid "Glass Effects — Dark Mode" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:88 +msgid "Glass Effects — Light Mode" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:24 +msgid "Glass Theme" +msgstr "" + +#: root/usr/share/luci/menu.d/luci-theme-glass.json:3 +msgid "Glass Theme Config" +msgstr "" + +#: ucode/template/themes/glass/header.ut:204 +msgid "Go to password configuration..." +msgstr "" + +#: root/usr/share/rpcd/acl.d/luci-theme-glass.json:3 +msgid "Grant access for luci-theme-glass" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:38 +msgid "Header status bar" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:111 +msgid "Invalid username and/or password! Please try again." +msgstr "" + +#: ucode/template/themes/glass/header.ut:211 +msgid "JavaScript required!" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:47 +msgid "Large (16px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:34 +msgid "Light" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:129 +msgid "Log in" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:51 +msgid "Login" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:131 +msgid "" +"No background set. Upload an image or video named \"bg\" (e.g. bg.jpg, " +"bg.png, bg.mp4) to /www/luci-static/glass/background/ via SCP." +msgstr "" + +#: ucode/template/themes/glass/header.ut:201 +msgid "No password set!" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:46 +msgid "Normal (14px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:97 +#: htdocs/luci-static/resources/view/system/glass.js:113 +msgid "Panel transparency" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:124 +msgid "Password" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:71 +msgid "Primary color (dark mode)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:55 +msgid "Primary color (light mode)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:44 +msgid "" +"Scales all text in the theme. Larger values improve readability on high-DPI " +"displays." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:39 +msgid "" +"Show live system stats (CPU, RAM, network, uptime) in the header. Disable to " +"reduce resource usage on low-end devices." +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:106 +msgid "Sign in to continue" +msgstr "登录以继续" + +#: htdocs/luci-static/resources/view/system/glass.js:45 +msgid "Small (13px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:31 +msgid "Theme mode" +msgstr "" + +#: ucode/template/themes/glass/header.ut:202 +msgid "" +"There is no password set on this router. Please configure a root password to " +"protect the web interface." +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:117 +msgid "Username" +msgstr "" + +#: ucode/template/themes/glass/header.ut:212 +msgid "" +"You must enable JavaScript in your browser or LuCI will not work properly." +msgstr "" diff --git a/luci-theme-glass/po/zh_Hant/glass.po b/luci-theme-glass/po/zh_Hant/glass.po new file mode 100644 index 000000000..a79fae99b --- /dev/null +++ b/luci-theme-glass/po/zh_Hant/glass.po @@ -0,0 +1,209 @@ +msgid "" +msgstr "" +"Language: zh_Hant\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: htdocs/luci-static/resources/view/system/glass.js:52 +msgid "Accent Colors" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:72 +msgid "Accent color used for active elements, links, and buttons in dark mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:56 +msgid "" +"Accent color used for active elements, links, and buttons in light mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:128 +msgid "Active" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:33 +msgid "Auto (follow system)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:108 +msgid "Backdrop blur radius for glass panels in dark mode." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:92 +msgid "" +"Backdrop blur radius for glass panels. Higher values create a more frosted " +"appearance." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:120 +msgid "Background" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:98 +msgid "" +"Background opacity of glass panels (0 = fully transparent, 1 = fully opaque)." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:114 +msgid "" +"Background opacity of glass panels in dark mode (0 = fully transparent, 1 = " +"fully opaque)." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:43 +msgid "Base font size" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:91 +#: htdocs/luci-static/resources/view/system/glass.js:107 +msgid "Blur intensity (px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:25 +msgid "" +"Configure the appearance of the Glass theme. Changes take effect after " +"saving and refreshing the page." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:32 +msgid "" +"Controls the overall appearance. \"Normal\" follows the system preference." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:123 +msgid "Current background" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:35 +msgid "Dark" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:48 +msgid "Extra Large (18px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:28 +msgid "General" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:104 +msgid "Glass Effects — Dark Mode" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:88 +msgid "Glass Effects — Light Mode" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:24 +msgid "Glass Theme" +msgstr "" + +#: root/usr/share/luci/menu.d/luci-theme-glass.json:3 +msgid "Glass Theme Config" +msgstr "" + +#: ucode/template/themes/glass/header.ut:204 +msgid "Go to password configuration..." +msgstr "" + +#: root/usr/share/rpcd/acl.d/luci-theme-glass.json:3 +msgid "Grant access for luci-theme-glass" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:38 +msgid "Header status bar" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:111 +msgid "Invalid username and/or password! Please try again." +msgstr "" + +#: ucode/template/themes/glass/header.ut:211 +msgid "JavaScript required!" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:47 +msgid "Large (16px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:34 +msgid "Light" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:129 +msgid "Log in" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:51 +msgid "Login" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:131 +msgid "" +"No background set. Upload an image or video named \"bg\" (e.g. bg.jpg, " +"bg.png, bg.mp4) to /www/luci-static/glass/background/ via SCP." +msgstr "" + +#: ucode/template/themes/glass/header.ut:201 +msgid "No password set!" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:46 +msgid "Normal (14px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:97 +#: htdocs/luci-static/resources/view/system/glass.js:113 +msgid "Panel transparency" +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:124 +msgid "Password" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:71 +msgid "Primary color (dark mode)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:55 +msgid "Primary color (light mode)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:44 +msgid "" +"Scales all text in the theme. Larger values improve readability on high-DPI " +"displays." +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:39 +msgid "" +"Show live system stats (CPU, RAM, network, uptime) in the header. Disable to " +"reduce resource usage on low-end devices." +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:106 +msgid "Sign in to continue" +msgstr "登入以繼續" + +#: htdocs/luci-static/resources/view/system/glass.js:45 +msgid "Small (13px)" +msgstr "" + +#: htdocs/luci-static/resources/view/system/glass.js:31 +msgid "Theme mode" +msgstr "" + +#: ucode/template/themes/glass/header.ut:202 +msgid "" +"There is no password set on this router. Please configure a root password to " +"protect the web interface." +msgstr "" + +#: ucode/template/themes/glass/sysauth.ut:117 +msgid "Username" +msgstr "" + +#: ucode/template/themes/glass/header.ut:212 +msgid "" +"You must enable JavaScript in your browser or LuCI will not work properly." +msgstr "" diff --git a/luci-theme-glass/root/etc/apk/keys/glass-apk.rsa.pub b/luci-theme-glass/root/etc/apk/keys/glass-apk.rsa.pub new file mode 100644 index 000000000..9fb45e4e0 --- /dev/null +++ b/luci-theme-glass/root/etc/apk/keys/glass-apk.rsa.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsUzec0+6lptCFX88129L +y7dDRwWkVL8tBIBwh67C2o54DN2AWgUkmuwnoZa+6UMtHFkyi8GLpw2ISNjiKzJn +GU8G21rg288OIQthhYIWVM3+eQ4J3yMA/r/1JQV80IDZ/awdfPRCjQQoJN96d5Km +gvKZeDp0UjcoD+gUy8S3LzCTNUwwjd9vnEZDTz1d+NckNM4JTTR1d+WFTwSeNAcE +jN/pNPPB/oyiUfyKLYHiDrBZputcnQ2suHC/DIVUSTQNhAyAst9UnUO3Jhv298HE +OrAhUZnwVC9F788Er+9mZAGpcqpPhZ+UDztOoO/aVt56ire/MBGLDkwYVN2W5wCo +QwIDAQAB +-----END PUBLIC KEY----- diff --git a/luci-theme-glass/root/etc/config/glass b/luci-theme-glass/root/etc/config/glass new file mode 100644 index 000000000..a4d097050 --- /dev/null +++ b/luci-theme-glass/root/etc/config/glass @@ -0,0 +1,10 @@ +config global 'global' + option mode 'normal' + option primary '#007AFF' + option dark_primary '#0A84FF' + option blur '20' + option transparency '0.72' + option blur_dark '25' + option transparency_dark '0.30' + option status_bar '1' + option font_size '14' diff --git a/luci-theme-glass/root/etc/opkg/keys/ef97c468af77cea1 b/luci-theme-glass/root/etc/opkg/keys/ef97c468af77cea1 new file mode 100644 index 000000000..8eca087ef --- /dev/null +++ b/luci-theme-glass/root/etc/opkg/keys/ef97c468af77cea1 @@ -0,0 +1,2 @@ +untrusted comment: luci-theme-glass package signing key public key +RWTvl8Ror3fOoUBhvj1JRuc+0SQ/8vIDJNCPrSXN5UNQJRGizXf/rxX/ diff --git a/luci-theme-glass/root/etc/uci-defaults/30_luci-theme-glass b/luci-theme-glass/root/etc/uci-defaults/30_luci-theme-glass new file mode 100755 index 000000000..8dfe630a2 --- /dev/null +++ b/luci-theme-glass/root/etc/uci-defaults/30_luci-theme-glass @@ -0,0 +1,12 @@ +#!/bin/sh + +# Always keep Glass in the theme list so it remains selectable in LuCI +uci -q set luci.themes.Glass=/luci-static/glass + +# Set as default only on fresh install -- don't stomp user choice on upgrade +[ "$PKG_UPGRADE" = 1 ] || uci -q set luci.main.mediaurlbase=/luci-static/glass + +uci -q commit luci +rm -f /tmp/luci-indexcache /tmp/luci-modulecache + +exit 0 diff --git a/luci-theme-glass/root/usr/share/luci/menu.d/luci-theme-glass.json b/luci-theme-glass/root/usr/share/luci/menu.d/luci-theme-glass.json new file mode 100644 index 000000000..a9155205b --- /dev/null +++ b/luci-theme-glass/root/usr/share/luci/menu.d/luci-theme-glass.json @@ -0,0 +1,15 @@ +{ + "admin/system/glass": { + "title": "Glass Theme Config", + "order": 90, + "action": { + "type": "view", + "path": "system/glass" + }, + "depends": { + "acl": [ + "luci-theme-glass" + ] + } + } +} diff --git a/luci-theme-glass/root/usr/share/rpcd/acl.d/luci-theme-glass.json b/luci-theme-glass/root/usr/share/rpcd/acl.d/luci-theme-glass.json new file mode 100644 index 000000000..460165924 --- /dev/null +++ b/luci-theme-glass/root/usr/share/rpcd/acl.d/luci-theme-glass.json @@ -0,0 +1,18 @@ +{ + "luci-theme-glass": { + "description": "Grant access for luci-theme-glass", + "read": { + "uci": [ "glass" ], + "file": { + "/www/luci-static/glass/background/*": [ "list" ] + }, + "ubus": { + "network.interface": [ "dump" ], + "network.device": [ "status" ] + } + }, + "write": { + "uci": [ "glass" ] + } + } +} diff --git a/luci-theme-glass/screenshots/.gitkeep b/luci-theme-glass/screenshots/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/luci-theme-glass/screenshots/Login_page_light.png b/luci-theme-glass/screenshots/Login_page_light.png new file mode 100644 index 000000000..aa961a439 Binary files /dev/null and b/luci-theme-glass/screenshots/Login_page_light.png differ diff --git a/luci-theme-glass/screenshots/Overview_page_dark.png b/luci-theme-glass/screenshots/Overview_page_dark.png new file mode 100644 index 000000000..763e547f7 Binary files /dev/null and b/luci-theme-glass/screenshots/Overview_page_dark.png differ diff --git a/luci-theme-glass/screenshots/Overview_page_light.png b/luci-theme-glass/screenshots/Overview_page_light.png new file mode 100644 index 000000000..c67635d9a Binary files /dev/null and b/luci-theme-glass/screenshots/Overview_page_light.png differ diff --git a/luci-theme-glass/screenshots/Software_page_light.png b/luci-theme-glass/screenshots/Software_page_light.png new file mode 100644 index 000000000..608f88e62 Binary files /dev/null and b/luci-theme-glass/screenshots/Software_page_light.png differ diff --git a/luci-theme-glass/screenshots/Traffic_graph_light.png b/luci-theme-glass/screenshots/Traffic_graph_light.png new file mode 100644 index 000000000..f6f5511af Binary files /dev/null and b/luci-theme-glass/screenshots/Traffic_graph_light.png differ diff --git a/luci-theme-glass/ucode/template/themes/glass/footer.ut b/luci-theme-glass/ucode/template/themes/glass/footer.ut new file mode 100644 index 000000000..c3eb01492 --- /dev/null +++ b/luci-theme-glass/ucode/template/themes/glass/footer.ut @@ -0,0 +1,185 @@ +{# luci-theme-glass — Main page footer template #} +{% + import { readfile, access } from 'fs'; + import { cursor } from 'uci'; + const glass_version = trim(readfile('/usr/share/ucode/luci/template/themes/glass/version')) ?? '?'; + let glass_status_bar = '1'; + let _cfg = cursor(); + if (access('/etc/config/glass')) { + glass_status_bar = _cfg.get_first('glass', 'global', 'status_bar') ?? '1'; + } +%} +{% if (!blank_page): %} + + + + + + + + +{% endif %} +{% if (js): %}{% endif %} + + diff --git a/luci-theme-glass/ucode/template/themes/glass/footer_login.ut b/luci-theme-glass/ucode/template/themes/glass/footer_login.ut new file mode 100644 index 000000000..5ff8d0638 --- /dev/null +++ b/luci-theme-glass/ucode/template/themes/glass/footer_login.ut @@ -0,0 +1 @@ +{# luci-theme-glass — Login page footer (empty, sysauth.ut is self-contained) #} diff --git a/luci-theme-glass/ucode/template/themes/glass/header.ut b/luci-theme-glass/ucode/template/themes/glass/header.ut new file mode 100644 index 000000000..80da2ef90 --- /dev/null +++ b/luci-theme-glass/ucode/template/themes/glass/header.ut @@ -0,0 +1,215 @@ +{# + luci-theme-glass — Main page header template + Apple-inspired glassmorphism theme for LuCI +-#} + +{% + import { readfile, access } from 'fs'; + import { cursor } from 'uci'; + import { getuid, getspnam } from 'luci.core'; + + const glass_version = trim(readfile('/usr/share/ucode/luci/template/themes/glass/version')) ?? '?'; + const boardinfo = ubus.call('system', 'board'); + const hostname = striptags(boardinfo?.hostname ?? 'OpenWrt'); + + http.prepare_content('text/html; charset=UTF-8'); + + /* Read theme config */ + let glass_mode = 'normal'; + let glass_primary = '#007AFF'; + let glass_dark_primary = '#0A84FF'; + let glass_blur = '20'; + let glass_transparency = '0.72'; + let glass_blur_dark = '25'; + let glass_transparency_dark = '0.30'; + let glass_font_size = '14'; + + let cfg = cursor(); + if (access('/etc/config/glass')) { + glass_mode = cfg.get_first('glass', 'global', 'mode') ?? glass_mode; + glass_primary = cfg.get_first('glass', 'global', 'primary') ?? glass_primary; + glass_dark_primary = cfg.get_first('glass', 'global', 'dark_primary') ?? glass_dark_primary; + glass_blur = cfg.get_first('glass', 'global', 'blur') ?? glass_blur; + glass_transparency = cfg.get_first('glass', 'global', 'transparency') ?? glass_transparency; + glass_blur_dark = cfg.get_first('glass', 'global', 'blur_dark') ?? glass_blur_dark; + glass_transparency_dark = cfg.get_first('glass', 'global', 'transparency_dark') ?? glass_transparency_dark; + glass_font_size = cfg.get_first('glass', 'global', 'font_size') ?? glass_font_size; + } + + /* Read dark.css for inline injection */ + const dark_css = readfile('/www/luci-static/glass/css/dark.css'); + + /* Find background media */ + let bg_src = null; + let bg_type = 'image'; + const bg_dir = '/www/luci-static/glass/background'; + const img_exts = ['.jpg', '.jpeg', '.png', '.gif', '.webp']; + const vid_exts = ['.mp4', '.webm']; + + for (let ext in img_exts) { + if (access(bg_dir + '/bg' + ext)) { + bg_src = media + '/background/bg' + ext; + break; + } + } + if (!bg_src) { + for (let ext in vid_exts) { + if (access(bg_dir + '/bg' + ext)) { + bg_src = media + '/background/bg' + ext; + bg_type = 'video'; + break; + } + } + } +-%} + + + + + + + {{ hostname }}{{ node?.title ? ` - ${striptags(node.title)}` : '' }} - LuCI + + + + {% if (access('/etc/config/glass')): %} + + {% endif %} + + {% if (node?.css): %} + + {% endif %} + {% if (css): %} + + {% endif %} + + + + + +
    + {% if (bg_src && bg_type == 'video'): %} + + {% elif (bg_src): %} + + {% endif %} +
    + +{% if (!blank_page): %} + + + + + + + + +
    +
    +
    + {% if (getuid() == 0 && getspnam('root')?.pwdp === ''): %} +
    +

    {{ _('No password set!') }}

    +

    {{ _('There is no password set on this router. Please configure a root password to protect the web interface.') }}

    + {% if (dispatcher.lookup("admin/system/admin")): %} + + {% endif %} +
    + {% endif %} + + +{% endif %} diff --git a/luci-theme-glass/ucode/template/themes/glass/header_login.ut b/luci-theme-glass/ucode/template/themes/glass/header_login.ut new file mode 100644 index 000000000..5da5f551a --- /dev/null +++ b/luci-theme-glass/ucode/template/themes/glass/header_login.ut @@ -0,0 +1 @@ +{# luci-theme-glass — Login header (not used, sysauth.ut is self-contained) #} diff --git a/luci-theme-glass/ucode/template/themes/glass/sysauth.ut b/luci-theme-glass/ucode/template/themes/glass/sysauth.ut new file mode 100644 index 000000000..0b2773a8f --- /dev/null +++ b/luci-theme-glass/ucode/template/themes/glass/sysauth.ut @@ -0,0 +1,160 @@ +{# luci-theme-glass — Login page (self-contained, argon-style) #} +{% + import { readfile, access } from 'fs'; + import { cursor } from 'uci'; + + const glass_version = trim(readfile('/usr/share/ucode/luci/template/themes/glass/version')) ?? '?'; + const boardinfo = ubus.call('system', 'board'); + const hostname = striptags(boardinfo?.hostname ?? 'OpenWrt'); + + http.prepare_content('text/html; charset=UTF-8'); + + /* Read theme config */ + let glass_mode = 'normal'; + let cfg = cursor(); + if (access('/etc/config/glass')) { + glass_mode = cfg.get_first('glass', 'global', 'mode') ?? glass_mode; + } + + /* Read dark.css */ + const dark_css = readfile('/www/luci-static/glass/css/dark.css'); + + /* Find background media */ + let bg_src = null; + let bg_type = 'image'; + const bg_dir = '/www/luci-static/glass/background'; + const img_exts = ['.jpg', '.jpeg', '.png', '.gif', '.webp']; + const vid_exts = ['.mp4', '.webm']; + + for (let ext in img_exts) { + if (access(bg_dir + '/bg' + ext)) { + bg_src = media + '/background/bg' + ext; + break; + } + } + if (!bg_src) { + for (let ext in vid_exts) { + if (access(bg_dir + '/bg' + ext)) { + bg_src = media + '/background/bg' + ext; + bg_type = 'video'; + break; + } + } + } +-%} + + + + + + + {{ _('Login') }} - {{ hostname }} - LuCI + + + + + + + + + +
    + {% if (bg_src && bg_type == 'video'): %} + + {% elif (bg_src): %} + + {% endif %} +
    + + + + + + + diff --git a/luci-theme-glass/ucode/template/themes/glass/version b/luci-theme-glass/ucode/template/themes/glass/version new file mode 100644 index 000000000..ab679818c --- /dev/null +++ b/luci-theme-glass/ucode/template/themes/glass/version @@ -0,0 +1 @@ +1.1.6 \ No newline at end of file diff --git a/luci-theme-liquid/.gitignore b/luci-theme-liquid/.gitignore new file mode 100644 index 000000000..4baff0ddc --- /dev/null +++ b/luci-theme-liquid/.gitignore @@ -0,0 +1,12 @@ +.research/ +.reasonix/ +*.ipk +*.apk + +# macOS +.DS_Store +.AppleDouble +.LSOverride +reasonix.toml +# CDP/headless 调试临时目录 +.liquid-tmp/ diff --git a/luci-theme-liquid/ChangeLogs.md b/luci-theme-liquid/ChangeLogs.md new file mode 100644 index 000000000..c71a7f074 --- /dev/null +++ b/luci-theme-liquid/ChangeLogs.md @@ -0,0 +1,61 @@ +# 更新日志 + +> 本文件归档 **[README.md](README.md)** 的全部历史更新条目;README 正文只保留最近一次更新。 +> English version: **[ChangeLogs_EN.md](ChangeLogs_EN.md)** + +- **2026-09-28 · v1.0(主版本升级)**:透明度调节 + 在线更新 + 登录体验 + 宽表格自适应 + - **玻璃透明度滑杆**:顶栏颜色区拖动 0–100 即时生效、点刻度回默认;菜单、按钮、主题色背景同步变化,亮 / 暗各有默认;登录页同样可调,设置存在路由器上、换设备保留 + - **在线检查更新**:点页脚版本号即可检查新版本、一键更新;检查中 / 已是最新 / 有新版本 / 失败 四种状态一目了然,失败会明确提示;手机页脚也显示版本、按钮加大 + - **登录页**:登录前调好的明暗 / 主题色 / 透明度,登录后弹提示一键应用(30 秒可忽略);logo 换上毛玻璃光晕;修复明暗切换闪动与误触提交 + - **下拉菜单**:玻璃外观重做,宽度随最长选项伸缩、贴边自动上弹、点内容不闪烁;暗色下选项更清楚;无线设置等页面异常的下拉恢复正常 + - **提示弹窗**:成功提示(如“密码已更改”)改为屏幕居中玻璃弹窗、6 秒自动关闭;手机上按钮自动换行不溢出 + - **宽表格自适应**:表格超出屏幕时先折叠操作按钮(一行一个)把宽度让给文字,文字不再被挤窄或裁掉;手机上表格收回屏内、无需左右拖动 + - **其他修复**:菜单滑块切分类不再停在原处;OpenClash 打开不再先闪黑;选中标签改高对比配色,暗色下更清楚 + +- **2026-08-18 · v0.7**:移动端体验优化 + 多处体验优化 + - **移动端体验优化** + - 滚动页面时浏览器工具栏 / 地址栏自动收缩,移动端获得更大视野 + - 底部横幅避让全面屏手势条 + - EasyTier 重启、刷新版本确认框在手机上居中显示 + - **多处体验优化** + - 修复按钮描边不贴合圆角、边缘出现竖缝的问题(普通按钮 / 保存并应用 / 分割按钮) +- **2026-08-15 · v0.6**:菜单搜索 + 多处体验优化 + - **菜单搜索**:颜色区新增搜索入口,点击推出搜索框,实时过滤菜单项,支持中英文匹配 + - **多处体验优化**:多实例配置(如 Dropbear)独立卡片显示;刷新按钮玻璃质感;iStoreOS 页面标签适配主题样式 +- **2026-08-13 · v0.5**:弹窗居中 + 状态条样式优化 + - **弹窗居中**:应用配置提示(正在等待 / 已应用 / 无待应用)与待应用变更窗口在移动端正确居中显示;超高表单弹窗(如添加 DHCP)保持正常滚动 + - **状态条样式**:磁盘 / 内存使用条加深主题色、玻璃渐变质感,条内显示已用 / 总量 +- **2026-08-08 · v0.3**:自定义主题色 + 颜色开关优化 + 移动端 / 暗色模式改进 + - **自定义主题色**:5 套预置色之外新增自定义色——点击彩虹圆点下拉输入 hex 颜色编码(`#RRGGBB`),离开输入框即保存并全局生效;非法输入自动回退默认蓝(仍保持自定义模式启用);经 uci 持久化,换设备保留 + - **颜色开关**:外框统一 35px 高;自定义色选中时圆点内部显示自定义色、外圈彩虹渐变描边(一眼识别);Bing 壁纸按钮方形描边贴合图标、启用时单层白色描边 + - **移动端**:首屏不再闪现"菜单先展开再收缩"(禁用桌面入场动画干扰抽屉收起态,加载完成即可操作);底部 foot 高度缩减 + - **暗色模式**:模式开关图标(外链 svg)反色为浅色,深色背景下清晰可见 + - **修复**:自定义色保存(controller 白名单支持 `accent=custom` / `accent_custom`)、登录页颜色开关定位、颜色区外框高度 + +- **2026-08-08 · v0.2-r52**:移动端体验优化 + 下拉菜单优化 + - **移动端接口页**:接口小框(ifacebox)与其所在行卡片左缘间隔优化为 4px,显示更整齐 + - **移动端层叠卡片缩进**:弹出窗口宽 95%、嵌套卡片每层向内缩 2px,展示更大操作区 + - **移动端菜单**:点击空白处退出菜单 + - **桌面端弹出式层叠卡片**:显示效果优化(modal 80% 宽、嵌套卡片逐层限宽防溢出) + - **下拉菜单**:单选 / 多选交互与显示效果优化 + +- **2026-08-07 · v0.2**:配置持久化(uci)+ Bing 每日壁纸 + 多项修复 + - **配置持久化(uci)**:亮/暗/自动模式、5 套主题色、Bing 开关统一存入 `uci /etc/config/liquid`,换客户端(浏览器/设备)仍保留;改动经 LuCI RPC 直接提交,立即生效、无"待应用"提示 + - **Bing 每日壁纸(锁屏)**:主题色前新增 Bing 开关按钮(字母 b logo);启用后锁屏显示 Bing 当日壁纸(缓存于各设备 `/tmp/liquid/`);无互联网或按钮关闭时自动回退默认壁纸(亮/暗各按模式) + - **锁屏只读**:登录页仅本地预览,不写入配置 + - **uci 优先**:修复本地 localStorage 旧值覆盖 uci 新配置的问题,页面加载即应用路由器端配置 + - **接口页修复**:未加入防火墙区域的接口 head 色条由白色改为灰色(亮色浅灰 / 暗色近黑),暗色下接口名不再不可见 + - **CI**:GitHub Actions 双平台(x86 + MT798x)× 双格式(ipk / apk)自动构建发布 + - 版本号 0.2-r1 + +- **2026-08-07 · v0.1 首次发布**:全新 macOS 风格 Liquid Glass(液态玻璃)LuCI 主题,带来以下功能: + - **液态玻璃设计语言**:侧栏、内容卡片、登录页、页脚统一 frosted-glass(模糊 + 高光 + 主题色光晕),暗色下玻璃更实、文字更清晰 + - **三态模式切换**:亮色 / 暗色 / 跟随系统,顶栏一键切换,`localStorage` 持久化、加载无闪烁 + - **主题色**:5 套主题色(蓝 / 玫红 / 橙黄 / 郁金香紫 / 黄绿),菜单、按钮、Tab、logo 全链路联动 + - **菜单**:默认全部折叠(点击一级菜单才展开二级)、hover 追踪滑块、选中玻璃胶囊、移动端滑出菜单 + - **下拉控件**:一体化渐变胶囊(主体 + 分隔箭头,同"保存并应用"),视口边缘自动向上弹出、不被页脚遮挡 + - **悬浮内容框(tooltip)**:磨砂玻璃底、顶层显示不被相邻卡片遮挡、边缘自动避让、互斥防残留 + - **表格**:行内单元格等高(分割线对齐)、移动端按内容宽度 + 横向滚动(长列不重叠) + - **接口 / 设备页**:SVG 图标统一、连接状态由图标文件区分(不再半透误导) + - **锁屏(登录页)**:macOS 风格玻璃登录卡片 + Monterey 壁纸 + 主题色水滴 logo + - **兼容**:LuCI ≥ 23(OpenWrt 23.05 / 24.10 / master) diff --git a/luci-theme-liquid/ChangeLogs_EN.md b/luci-theme-liquid/ChangeLogs_EN.md new file mode 100644 index 000000000..ed157ef6b --- /dev/null +++ b/luci-theme-liquid/ChangeLogs_EN.md @@ -0,0 +1,61 @@ +# Changelog + +> Full history of the entries from **[README_EN.md](README_EN.md)**; the README body only keeps the latest one. +> 中文版本:**[ChangeLogs.md](ChangeLogs.md)** + +- **2026-09-28 · v1.0 (major release)**: opacity slider + online update + lock screen polish + wide-table adaptivity + - **Glass opacity slider**: drag 0–100 in the top-bar color area for an instant preview, click the tick to reset; menus, buttons and accent backgrounds follow along, with separate light / dark defaults; also on the lock screen, and the setting stays on your router across devices + - **Online update check**: click the footer version to check and install a new release; checking / up to date / update available / failed are all clearly shown, and failures report an error instead of hanging; mobile footers show the version too, with bigger buttons + - **Lock screen**: mode, accent and opacity picked before signing in are offered with one click after login (30-second countdown, dismissible); the logo gets a frosted glass glow; fixed mode-switch flicker and accidental submits + - **Dropdowns**: refreshed glass look — width follows the longest option, menus flip up near the edge, no flash on click; clearer highlights in dark mode; previously broken dropdowns (e.g. wireless settings) work again + - **Notifications**: success messages (e.g. "password changed") now show as a centered glass dialog that closes after 6 seconds; buttons wrap on phones instead of overflowing + - **Wide tables**: when a table no longer fits the screen, action buttons stack one per line so text keeps its width — no more squeezed or clipped columns; on phones the table stays on screen with no sideways scrolling + - **Fixes**: the sidebar slider no longer sticks to the previous category; OpenClash no longer flashes dark on open; the selected tab uses a high-contrast color for dark mode + +- **2026-08-18 · v0.7**: mobile UX polish + general UX polish + - **Mobile** + - Toolbar / address bar auto-collapses on scroll, giving a larger view on phones + - Bottom banner clears the fullscreen gesture bar + - EasyTier restart / refresh-version dialogs center on mobile + - **General** + - Fixed button borders not hugging rounded corners / edge seams (plain buttons, Save & Apply, split-select) +- **2026-08-15 · v0.6**: menu search + UX polish + - **Menu search**: new search entry in the color area — opens a search box that filters menu items in real time, matching Chinese and English titles + - **UX polish**: multi-instance configs (e.g. Dropbear) show as separate cards; refresh button gets a frosted glass look; iStoreOS page tabs adopt the theme tab style +- **2026-08-13 · v0.5**: centered modals + status-bar style polish + - **Centered modals**: apply-progress toasts (applying / applied / nothing to apply) and the pending-changes dialog are now properly centered on mobile; tall form modals (e.g. add DHCP) keep normal scrolling + - **Status bars**: disk / memory usage bars get a deeper accent color with a glass gradient, showing used / total inline +- **2026-08-08 · v0.3**: custom accent color + color-switch polish + mobile / dark-mode improvements + - **Custom accent color**: beyond the 5 presets — click the rainbow dot to drop a hex input (`#RRGGBB`); leaving the field saves and applies instantly; invalid values fall back to default blue (custom mode stays enabled); persisted via uci across clients + - **Color switch**: pill fixed at 35px; a selected custom dot shows its color inside with a rainbow ring for instant recognition; the Bing wallpaper button gets a square outline hugging its icon and a single white ring when active + - **Mobile**: no first-load menu flash (desktop entry animation disabled so the drawer starts closed and usable immediately); footer height trimmed + - **Dark mode**: mode-switch icons (external svg) inverted to light so they stay visible on dark backgrounds + - **Fixes**: custom-color persistence (controller whitelist accepts `accent=custom` / `accent_custom`), login-page color-switch position, pill height + +- **2026-08-08 · v0.2-r52**: mobile UX & dropdown improvements + - **Mobile interfaces page**: ifacebox aligned 4px from its row card's left edge for a cleaner look + - **Mobile nested-card insets**: modal at 95% width, each nested layer steps in 2px, giving a larger usable area + - **Mobile menu**: tapping outside closes the menu + - **Desktop modal / nested cards**: improved display (modal at 80% width, nested layers width-constrained to prevent overflow) + - **Dropdowns**: single-select & multi-select interaction and display polish + +- **2026-08-07 · v0.2**: persistent config (uci) + Bing daily wallpaper + fixes + - **Persistent config (uci)**: light/dark/auto mode, the 5 accent colors and the Bing switch are stored in `uci /etc/config/liquid`, so the settings follow the router across clients (browsers / devices); changes are committed through the theme's own ucode endpoint and take effect immediately with no "pending apply" entries or save prompts. + - **Bing daily wallpaper (lock screen)**: a Bing toggle button (letter-b logo) sits before the five accent colors; when enabled, the lock screen shows Bing's picture of the day (cached per device in `/tmp/liquid/`); when offline or disabled it falls back to the default wallpaper (per mode). + - **Lock screen read-only**: the login page only previews locally and never writes configuration. + - **uci wins**: fixed stale localStorage overriding the uci config — every reload applies the router-wide config. + - **Interfaces page fix**: the head bar of interfaces not assigned to any firewall zone is now gray (light: light gray / dark: near-black) so the interface name stays readable in dark mode. + - **CI**: GitHub Actions build & release on two platforms (x86 + MT798x) × two formats (ipk / apk). + - Version bumped to 0.2-r1. + +- **2026-08-07 · v0.1 (initial release)**: a brand-new macOS-style Liquid Glass LuCI theme + - **Liquid glass design language**: frosted glass (blur + highlight + theme glow) across sidebar, content cards, login page and footer + - **Three-way mode**: light / dark / auto from the top bar, persisted, no flash on load + - **Accent colors**: 5 themes driving menus, buttons, tabs and logo + - **Menu**: collapsed by default, hover-tracking slider, selected glass capsule, mobile slide-out menu + - **Dropdowns**: gradient capsule (body + divider + arrow, same as "Save & Apply"), auto-flip at viewport edges + - **Tooltips**: frosted glass, top-level display, edge avoidance + - **Tables**: equal-height rows, horizontal scroll on mobile (no long-column overlap) + - **Interfaces / Devices**: uniform SVG icons, link state told by the icon file + - **Lock screen**: macOS-style frosted login card + Monterey wallpaper + accent-colored waterdrop logo + - **Compatibility**: LuCI ≥ 23 (OpenWrt 23.05 / 24.10 / master) diff --git a/luci-theme-liquid/LICENSE b/luci-theme-liquid/LICENSE new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/luci-theme-liquid/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/luci-theme-liquid/Makefile b/luci-theme-liquid/Makefile new file mode 100644 index 000000000..262c3c673 --- /dev/null +++ b/luci-theme-liquid/Makefile @@ -0,0 +1,67 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-theme-liquid +PKG_VERSION:=1.0 +PKG_RELEASE:=1 + +PKG_MAINTAINER:=然后七年 +PKG_LICENSE:=Apache-2.0 + +# OpenWrt 23.05 的 luci.mk 版本规则只取 PKG_VERSION(忽略 PKG_RELEASE), +# 导致 GitHub Action 编译出的 ipk 没有 r 小版本(0.4 vs 0.4-r1)。 +# 用 override VERSION 强制统一为 PKG_VERSION-rPKG_RELEASE。注意:必须 +# 写在 include luci.mk 之前——luci.mk 末尾会立即 eval BuildPackage, +# 其 ipk 命名/control 里的 $(VERSION) 在那一刻固化,写后面就晚了。 +# luci.mk 的 VERSION:= 是普通赋值(被 override 压住),且本值在新版 +# luci.mk 与 i18n 子包(PKG_PO_VERSION)下与默认一致,不影响 apk/新版。 +override VERSION:=$(if $(PKG_RELEASE),$(PKG_VERSION)-r$(PKG_RELEASE),$(PKG_VERSION)) + +LUCI_TITLE:=Liquid glass theme for LuCI (>= 23) +LUCI_PKGARCH:=all +LUCI_DEPENDS:=+luci-base + +# 汉化/子包版本与主包保持同步 +PKG_PO_VERSION:=$(PKG_VERSION)-r$(PKG_RELEASE) + +# csstidy 会破坏 @media 块(只保留首条规则,其余泄漏到块外无条件生效), +# 导致移动端断点样式全部失效;本主题关闭 CSS 压缩,样式原样打包。 +CONFIG_LUCI_CSSTIDY:= + +define Package/$(PKG_NAME)/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + # 公钥信任不在此处写(去冗余):apk 装机时生成的 post-install 会先 + # 调 default_postinst —— 它会执行包内 etc/uci-defaults/99-zed-apk-key-liquid + # 完成公钥条件写入并消费该脚本;固件场景由首次开机执行同一脚本覆盖 + # (构建期 --no-scripts 不跑脚本,文件随镜像保留);装 r9 之前的设备 + # 由 OTA 安装前自举兜底。23.05 为 opkg 系统,不使用 apk 钥匙。 + + # 23.05 opkg 不执行 uci-defaults,必须在 postinst 中设置主题配置。 + # 确保 mediaurlbase 指向 liquid,否则 fallback 到 null。 + if [ "$$(uci -q get luci.main.mediaurlbase)" != "/luci-static/liquid" ]; then + uci set luci.main.mediaurlbase=/luci-static/liquid + uci commit luci + fi + + rm -f /tmp/luci-indexcache.* + rm -rf /tmp/luci-modulecache/ + /etc/init.d/rpcd reload 2>/dev/null + exit 0 +} +exit 0 +endef + +define Package/$(PKG_NAME)/postrm +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + uci -q delete luci.themes.Liquid + [ "$$(uci -q get luci.main.mediaurlbase)" = "/luci-static/liquid" ] && \ + uci -q delete luci.main.mediaurlbase + uci commit luci +} +exit 0 +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-theme-liquid/README.md b/luci-theme-liquid/README.md new file mode 100644 index 000000000..ad2095c70 --- /dev/null +++ b/luci-theme-liquid/README.md @@ -0,0 +1,177 @@ + + +[license]: /LICENSE +[license-badge]: https://img.shields.io/github/license/zzsj0928/luci-theme-liquid?style=flat-square&a=1 +[prs]: https://github.com/zzsj0928/luci-theme-liquid/pulls +[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square +[issues]: https://github.com/zzsj0928/luci-theme-liquid/issues/new +[issues-badge]: https://img.shields.io/badge/Issues-welcome-brightgreen.svg?style=flat-square +[release]: https://github.com/zzsj0928/luci-theme-liquid/releases +[release-badge]: https://img.shields.io/github/v/release/zzsj0928/luci-theme-liquid?style=flat-square +[download]: https://github.com/zzsj0928/luci-theme-liquid/releases +[download-badge]: https://img.shields.io/github/downloads/zzsj0928/luci-theme-liquid/total?style=flat-square +[en-us-link]: /README_EN.md +[zh-cn-link]: /README.md +[official]: https://github.com/openwrt/openwrt +[immortalwrt]: https://github.com/immortalwrt/immortalwrt +[luci-mod]: https://github.com/xylz0928/luci-mod + +
    +

    + +# 💧 luci-theme-liquid + +**macOS 风格 Liquid Glass(液态玻璃)OpenWrt LuCI 主题**,适用于 **LuCI ≥ 23**(OpenWrt 23.05 / 24.10 / master)。 + +支持**亮色 / 暗色 / 跟随系统**三态切换、**5 套主题色 + 自定义颜色**、可调玻璃模糊与透明度;登录页、侧栏、内容卡片、下拉、悬浮框全链路液态玻璃质感。 + +[![license][license-badge]][license] +[![prs][prs-badge]][prs] +[![issues][issues-badge]][issues] +[![release][release-badge]][release] +[![download][download-badge]][download] + +**简体中文** | [English][en-us-link] + +[特性](#特性) • +[更新日志](#更新日志) • +[兼容性](#兼容性) • +[编译安装](#编译安装) • +[界面展示](#界面展示) • +[致谢](#致谢) + + + +
    + +## 特性 + +- **液态玻璃设计语言**:侧栏、内容卡片、登录卡片、页脚统一 frosted-glass(模糊 + 高光 + 主题色光晕),暗色下玻璃更实、文字更清晰。 +- **亮 / 暗 / 跟随系统**:顶栏右侧一键切换,设置存在路由器上(换浏览器、换设备仍在),加载即生效、无闪烁;锁屏页同样可切。 +- **主题色**:**远空蓝**、**勃艮第红**、**落日金**、**暮山紫**、**青金萃** 5 套 + 自定义色(点彩虹圆点输入 `#RRGGBB`),选中菜单、悬停滑块、按钮、Tab、logo 全链路联动。 +- **玻璃透明度滑杆**:顶栏颜色区拖动 0–100 实时调节玻璃深浅,点默认刻度一键还原;亮 / 暗各有默认值;菜单、按钮、主题色背景同步变化;锁屏页同样可调。 +- **菜单搜索**:顶栏搜索按钮下推抽屉式搜索框,输入即实时过滤菜单,中文名与英文原题都能匹配,点外部或再点按钮收回。 +- **左侧菜单**:默认全部折叠,点一级才展开二级;悬停追踪滑块 + 选中玻璃胶囊;多实例配置(如 Dropbear)自动拆成独立卡片;移动端滑出菜单避让顶栏、点空白关闭。 +- **下拉控件**:玻璃胶囊外观,宽度随最长选项自动伸缩、贴近窗口边缘自动向上弹出、点内容区不闪烁;暗色下选项高亮更清楚。 +- **表格自适应**:行内单元格等高;表格超出屏幕时先把操作按钮竖排(一行一个)把宽度让给文字,文字不再被挤窄或裁掉;手机上表格收回屏内、无需左右拖动。 +- **接口 / 设备页**:接口图标统一 24px、始终不透明(连接状态由图标本身区分);行内等高,按钮平时一行、放不下时自动竖排让位文字;移动端接口小框与所在行卡片保持 4px 间隔。 +- **移动端适配**:弹出窗口 95% 宽、嵌套卡片逐层向内缩进;抽屉菜单点空白关闭、首屏直接收起无闪烁;提示按钮自动换行不溢出。 +- **在线检查更新**:点页脚版本号即可检查新版本、一键更新,检查中 / 已是最新 / 有新版本 / 失败 四态清晰;手机页脚同样显示主题版本。 +- **提示弹窗**:成功提示(如“密码已更改”)升级为屏幕居中的玻璃弹窗,6 秒自动关闭。 +- **锁屏(登录页)**:macOS 风格玻璃登录卡片 + Monterey 壁纸(亮/暗)+ 可选 **Bing 每日壁纸**(每日自动抓取缓存)+ 毛玻璃光晕水滴 logo;登录前调好的明暗 / 主题色 / 透明度,登录成功后一键应用。 +- **悬浮内容框(tooltip)**:磨砂玻璃底、置顶显示(不被相邻卡片遮挡)、边缘自动避让、互斥防残留。 +- **SVG 图标**:网络 / 接口状态图标取自 [xylz0928/luci-mod][luci-mod] `immortalwrt-24.10` 分支的 SVG 图标集;UI 元素图标(sun / moon / auto / refresh / lock / search / close / chevron)为内置 SVG。 + + +## 更新日志 + +- **2026-09-28 · v1.0(主版本升级)**:透明度调节 + 在线更新 + 登录体验 + 宽表格自适应 + - **玻璃透明度滑杆**:顶栏颜色区拖动 0–100 即时生效、点刻度回默认;菜单、按钮、主题色背景同步变化,亮 / 暗各有默认;登录页同样可调,设置存在路由器上、换设备保留 + - **在线检查更新**:点页脚版本号即可检查新版本、一键更新;检查中 / 已是最新 / 有新版本 / 失败 四种状态一目了然,失败会明确提示;手机页脚也显示版本、按钮加大 + - **登录页**:登录前调好的明暗 / 主题色 / 透明度,登录后弹提示一键应用(30 秒可忽略);logo 换上毛玻璃光晕;修复明暗切换闪动与误触提交 + - **下拉菜单**:玻璃外观重做,宽度随最长选项伸缩、贴边自动上弹、点内容不闪烁;暗色下选项更清楚;无线设置等页面异常的下拉恢复正常 + - **提示弹窗**:成功提示(如“密码已更改”)改为屏幕居中玻璃弹窗、6 秒自动关闭;手机上按钮自动换行不溢出 + - **宽表格自适应**:表格超出屏幕时先折叠操作按钮(一行一个)把宽度让给文字,文字不再被挤窄或裁掉;手机上表格收回屏内、无需左右拖动 + - **其他修复**:菜单滑块切分类不再停在原处;OpenClash 打开不再先闪黑;选中标签改高对比配色,暗色下更清楚 + +> 📜 完整更新日志(v0.1 起全部版本)见 **[ChangeLogs.md](ChangeLogs.md)**。 + +## 兼容性 + +支持基于 [OpenWrt 官方][official] 与 [ImmortalWrt][immortalwrt] 的现代 LuCI 环境(LuCI ≥ 23,OpenWrt 23.05 / 24.10 / master)。 + +## 编译安装 + +本包使用 LuCI feed 的 `luci.mk` 打包规则(`include $(TOPDIR)/feeds/luci/luci.mk`),因此编译环境需先 `./scripts/feeds update -a && ./scripts/feeds install -a`(含 luci feed)。 + +把本目录放入 OpenWrt 源码树的 `package/luci-theme-liquid/`(或 `feeds/luci/themes/luci-theme-liquid/`),在 `.config` 中启用该包后编译: + +```bash +cd openwrt/package +git clone https://github.com/xylz0928/luci-theme-liquid.git +make menuconfig # 选择 LUCI → Themes → luci-theme-liquid +make -j1 V=s +``` + +或直接编译单个包: + +```sh +# 在 .config 中启用(任选其一) +make menuconfig # LuCI → Themes → luci-theme-liquid +# 或 +echo 'CONFIG_PACKAGE_luci-theme-liquid=y' >> .config && make defconfig + +make package/luci-theme-liquid/compile -j4 V=s +``` + +> 注意:`make package//compile` 只对已在 `.config` 中启用(`=y`/`=m`)的包真正执行构建,未启用时 make 会空转(`Entering/Leaving` 无任何动作),不要误判为成功。 + +生成的 `bin/packages//base/luci-theme-liquid-0.3-r.apk`(或 `.ipk`)可通过 `apk` / `opkg` 安装: + +```sh +apk add --allow-untrusted luci-theme-liquid-0.3-r1.apk +``` + +首次安装会自动注册主题(`luci.themes.Liquid=/luci-static/liquid`);若 `luci.main.mediaurlbase` 尚未配置则自动设为该主题,否则请在 **System → Advanced → Theme** 中手动选择 **Liquid**。 + +### 目录结构 + +``` +luci-theme-liquid/ +├── Makefile # OpenWrt 包(luci feed 内/独立 package 目录均可编译) +├── logo.svg # 主题 logo(水滴 + Liquid) +├── htdocs/luci-static/liquid/ # 主题媒体资源(/luci-static/liquid/) +│ ├── cascade.css # 全部样式(变量 / 菜单 / tab / cbi / dashboard / 锁屏) +│ ├── main.js # 模式开关注入、下拉避让、tooltip portal 等 +│ ├── logo.svg / logo.png # 主题 logo +│ ├── img/ # 锁屏壁纸(MontereyDark / MontereyLight) +│ ├── icons/ # 网络状态 SVG 图标(来自 luci-mod immortalwrt-24.10) +│ └── svg/ # UI 元素 SVG + dashboard 图标 +├── htdocs/luci-static/resources/ # 共享资源(/luci-static/resources/) +│ ├── menu-liquid.js # 左侧菜单 / tab 渲染(L.require('menu-liquid')) +│ └── view/liquid/sysauth.js # 锁屏登录视图(ui.showModal 'login') +├── ucode/template/themes/liquid/ # 主题 ucode 模板 +│ ├── header.ut / footer.ut # 页面骨架(uci 配置读取 / 防闪烁 / Bing 壁纸缓存) +│ └── sysauth.ut # 登录页模板(blank_page + 居中登录框) +└── root/ + ├── etc/uci-defaults/ # 安装时注册 luci.themes.Liquid + └── usr/share/ucode/luci/controller/liquid.uc # 主题配置保存端点(/admin/system/liquid/save_config) +``` + +## 界面展示 + +- 桌面端 + +![桌面端-锁屏-暗黑模式](https://raw.githubusercontent.com/zzsj0928/ReadmeContents/master/liquid/luci-theme-liquid_pc-lock-dark.png) +![桌面端-主界面-暗黑模式](https://raw.githubusercontent.com/zzsj0928/ReadmeContents/master/liquid/luci-theme-liquid_pc-mainpage-dark.png) +![桌面端-锁屏-明亮模式](https://raw.githubusercontent.com/zzsj0928/ReadmeContents/master/liquid/luci-theme-liquid_pc-lock-light.png) +![桌面端-主界面-明亮模式](https://raw.githubusercontent.com/zzsj0928/ReadmeContents/master/liquid/luci-theme-liquid_pc-mainpage-light.png) + + +- 移动端 + +![移动端-暗黑模式](https://raw.githubusercontent.com/zzsj0928/ReadmeContents/master/liquid/luci-theme-liquid_mobile-dark.jpg) +![移动端-明亮模式](https://raw.githubusercontent.com/zzsj0928/ReadmeContents/master/liquid/luci-theme-liquid_mobile-light.jpg) + +## 致谢 + +- 壁纸:macOS Monterey(Bright/Dark),来自 [xylz0928/luci-mod][luci-mod] `Background/`。 +- 网络状态 SVG 图标:来自 [xylz0928/luci-mod][luci-mod] `immortalwrt-24.10` 分支 `feeds/luci/modules/luci-base/htdocs/luci-static/resources/icons/`。 +- dashboard 模块图标(router / internet / wireless / devices):来自 OpenWrt 官方 [luci-mod-dashboard](https://github.com/openwrt/luci/tree/master/modules/luci-mod-dashboard)。 +- 菜单渲染逻辑参考 [luci-theme-openwrt-2020](https://github.com/openwrt/luci/tree/master/themes/luci-theme-openwrt-2020),锁屏视图参考 [luci-theme-bootstrap](https://github.com/openwrt/luci/tree/master/themes/luci-theme-bootstrap)(Apache-2.0)。 + +## 🍡 赏我一把 Token +**制作不易,感谢支持** +![赞赠](https://raw.githubusercontent.com/zzsj0928/ReadmeContents/master/general/donate-zed.jpg) + +## License + +Apache-2.0 diff --git a/luci-theme-liquid/README_EN.md b/luci-theme-liquid/README_EN.md new file mode 100644 index 000000000..086768179 --- /dev/null +++ b/luci-theme-liquid/README_EN.md @@ -0,0 +1,178 @@ + + +[license]: /LICENSE +[license-badge]: https://img.shields.io/github/license/zzsj0928/luci-theme-liquid?style=flat-square&a=1 +[prs]: https://github.com/zzsj0928/luci-theme-liquid/pulls +[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square +[issues]: https://github.com/zzsj0928/luci-theme-liquid/issues/new +[issues-badge]: https://img.shields.io/badge/Issues-welcome-brightgreen.svg?style=flat-square +[release]: https://github.com/zzsj0928/luci-theme-liquid/releases +[release-badge]: https://img.shields.io/github/v/release/zzsj0928/luci-theme-liquid?style=flat-square +[download]: https://github.com/zzsj0928/luci-theme-liquid/releases +[download-badge]: https://img.shields.io/github/downloads/zzsj0928/luci-theme-liquid/total?style=flat-square +[en-us-link]: /README_EN.md +[zh-cn-link]: /README.md +[official]: https://github.com/openwrt/openwrt +[immortalwrt]: https://github.com/immortalwrt/immortalwrt +[luci-mod]: https://github.com/xylz0928/luci-mod + +
    +

    + +# 💧 luci-theme-liquid + +**macOS-style Liquid Glass OpenWrt LuCI theme**, for **LuCI ≥ 23** (OpenWrt 23.05 / 24.10 / master). + +Supports **light / dark / auto** modes, **5 accent colors + a custom color picker** and adjustable blur & transparency; frosted glass design across the login page, sidebar, content cards, dropdowns and tooltips. + +[![license][license-badge]][license] +[![prs][prs-badge]][prs] +[![issues][issues-badge]][issues] +[![release][release-badge]][release] +[![download][download-badge]][download] + +**English** | [简体中文][zh-cn-link] + +[Features](#features) • +[Changelog](#changelog) • +[Compatibility](#compatibility) • +[Building & Installation](#building--installation) • +[Screenshots](#screenshots) • +[Credits](#credits) + + +
    + +## Features + +- **Liquid glass design language**: sidebar, content cards, login card and footer share a frosted-glass look (blur + highlight + accent glow); the dark mode glass is more solid for readable text. +- **Light / dark / system**: one tap in the top bar; settings are stored on your router so they survive new browsers and devices; applied before paint with no flash — the lock screen has the switch too. +- **Accent colors**: **Distant Blue**, **Burgundy Red**, **Sunset Gold**, **Dusk Purple**, **Peridot Essence**, plus a custom hex color (`#RRGGBB`) — menus, hover slider, buttons, tabs and the logo all follow. +- **Glass opacity slider**: drag 0–100 in the top-bar color area to tune the glass live, click the default tick to reset; separate light / dark defaults; menus, buttons and accent backgrounds follow; also on the lock screen. +- **Menu search**: the top-bar search button pushes down a drawer-style search box that filters menu items as you type, matching both Chinese names and English titles; click outside or the button again to close. +- **Sidebar menu**: all top-level menus collapsed by default — click a level-1 menu to expand its submenu; hover-tracking slider + selected glass capsule; multi-instance entries (e.g. Dropbear) split into their own cards; the mobile slide-out menu avoids the top bar and closes on outside taps. +- **Dropdown controls**: glass capsule look, width follows the longest option, the opened list flips upward near the window edge and never flashes when clicking the content area; clearer highlights in dark mode. +- **Adaptive tables**: equal-height cells per row; when a table no longer fits the screen, action buttons stack one per line so text keeps its width — no squeezed or clipped columns; on phones the table stays inside the screen with no sideways scrolling. +- **Interfaces / Devices pages**: uniform 24px interface icons, forced opaque (link state is told by the icon itself); rows keep equal heights, buttons stay on one line and stack only when space runs short; interface boxes keep a 4px gap from their row card on mobile. +- **Mobile adaptation**: modals at 95% width with nested cards stepping in per layer; the drawer menu closes on tapping outside and starts collapsed with no first-load flash; toast buttons wrap instead of overflowing. +- **Online update check**: click the footer version to check for a new release and install it in one click, with four clear states (checking / up to date / update available / failed); mobile footers show the theme version too. +- **Notifications**: success messages (e.g. "password changed") appear as a centered glass dialog that closes after 6 seconds. +- **Lock screen (login)**: macOS-style frosted login card + Monterey wallpaper (light/dark) + optional **Bing daily wallpaper** (auto-fetched and cached) + a glowing waterdrop logo; mode, accent and opacity picked before signing in can be applied with one click after login. +- **Tooltips**: frosted glass background, shown at the page top level (never hidden behind a neighbouring card), auto-avoid viewport edges, mutual exclusion against stale popups. +- **SVG icons**: network / interface status icons taken from [xylz0928/luci-mod][luci-mod] `immortalwrt-24.10`; UI element icons (sun / moon / auto / refresh / lock / search / close / chevron) are built-in SVGs. + + +## Changelog + +- **2026-09-28 · v1.0 (major release)**: opacity slider + online update + lock screen polish + wide-table adaptivity + - **Glass opacity slider**: drag 0–100 in the top-bar color area for an instant preview, click the tick to reset; menus, buttons and accent backgrounds follow along, with separate light / dark defaults; also on the lock screen, and the setting stays on your router across devices + - **Online update check**: click the footer version to check and install a new release; checking / up to date / update available / failed are all clearly shown, and failures report an error instead of hanging; mobile footers show the version too, with bigger buttons + - **Lock screen**: mode, accent and opacity picked before signing in are offered with one click after login (30-second countdown, dismissible); the logo gets a frosted glass glow; fixed mode-switch flicker and accidental submits + - **Dropdowns**: refreshed glass look — width follows the longest option, menus flip up near the edge, no flash on click; clearer highlights in dark mode; previously broken dropdowns (e.g. wireless settings) work again + - **Notifications**: success messages (e.g. "password changed") now show as a centered glass dialog that closes after 6 seconds; buttons wrap on phones instead of overflowing + - **Wide tables**: when a table no longer fits the screen, action buttons stack one per line so text keeps its width — no more squeezed or clipped columns; on phones the table stays on screen with no sideways scrolling + - **Fixes**: the sidebar slider no longer sticks to the previous category; OpenClash no longer flashes dark on open; the selected tab uses a high-contrast color for dark mode + +> 📜 Full changelog (all versions since v0.1) lives in **[ChangeLogs_EN.md](ChangeLogs_EN.md)**. + +## Compatibility + +Targets modern LuCI environments based on [OpenWrt][official] and [ImmortalWrt][immortalwrt] (LuCI ≥ 23, OpenWrt 23.05 / 24.10 / master). + +## Building & Installation + +This package uses LuCI's `luci.mk` packaging (`include $(TOPDIR)/feeds/luci/luci.mk`), so the build tree needs `./scripts/feeds update -a && ./scripts/feeds install -a` (includes the luci feed). + +Put this directory into `package/luci-theme-liquid/` (or `feeds/luci/themes/luci-theme-liquid/`) of the OpenWrt source tree, enable the package in `.config`, then build: + +```bash +cd openwrt/package +git clone https://github.com/xylz0928/luci-theme-liquid.git +make menuconfig # choose LUCI → Themes → luci-theme-liquid +make -j1 V=s +``` + +Or build only this package: + +```sh +# enable it in .config (either way) +make menuconfig # LuCI → Themes → luci-theme-liquid +# or +echo 'CONFIG_PACKAGE_luci-theme-liquid=y' >> .config && make defconfig + +make package/luci-theme-liquid/compile -j4 V=s +``` + +> Note: `make package//compile` only really builds packages enabled (`=y`/`=m`) in `.config`; otherwise make no-ops (just prints `Entering/Leaving`), which is not a successful build. + +The produced `bin/packages//base/luci-theme-liquid-0.3-r.apk` (or `.ipk`) can be installed with `apk` / `opkg`: + +```sh +apk add --allow-untrusted luci-theme-liquid-0.3-r1.apk +``` + +On first install the theme is registered automatically (`luci.themes.Liquid=/luci-static/liquid`); if `luci.main.mediaurlbase` is not set it is set to the theme, otherwise pick **Liquid** manually at **System → Advanced → Theme**. + +### Directory layout + +``` +luci-theme-liquid/ +├── Makefile # OpenWrt package (works inside luci feed or a standalone package dir) +├── logo.svg # theme logo (waterdrop + Liquid) +├── htdocs/luci-static/liquid/ # theme assets (/luci-static/liquid/) +│ ├── cascade.css # all styles (vars / menu / tab / cbi / dashboard / lock screen) +│ ├── main.js # mode switch, dropdown flip, tooltip portal, config saving +│ ├── logo.svg / logo.png # theme logo +│ ├── img/ # lock screen wallpapers (MontereyDark / MontereyLight) +│ ├── icons/ # network status SVG icons (from luci-mod immortalwrt-24.10) +│ └── svg/ # UI element SVGs + dashboard icons +├── htdocs/luci-static/resources/ # shared resources (/luci-static/resources/) +│ ├── menu-liquid.js # sidebar menu / tab rendering (L.require('menu-liquid')) +│ └── view/liquid/sysauth.js # lock screen login view (ui.showModal 'login') +├── root/usr/share/ucode/luci/ # ucode controller (config save endpoint) +│ └── controller/liquid.uc +├── root/usr/share/luci/menu.d/ # dispatcher route registration +│ └── liquid.json +├── ucode/template/themes/liquid/ # theme ucode templates +│ ├── header.ut / footer.ut # page skeleton (uci config / anti-flash / Bing wallpaper cache) +│ └── sysauth.ut # login template (blank_page + centered login box) +└── root/etc/uci-defaults/ # registers luci.themes.Liquid on install +``` + +## Screenshots + +- 桌面端 + +![桌面端-锁屏-暗黑模式](https://raw.githubusercontent.com/zzsj0928/ReadmeContents/master/liquid/luci-theme-liquid_pc-lock-dark.png) +![桌面端-主界面-暗黑模式](https://raw.githubusercontent.com/zzsj0928/ReadmeContents/master/liquid/luci-theme-liquid_pc-mainpage-dark.png) +![桌面端-锁屏-明亮模式](https://raw.githubusercontent.com/zzsj0928/ReadmeContents/master/liquid/luci-theme-liquid_pc-lock-light.png) +![桌面端-主界面-明亮模式](https://raw.githubusercontent.com/zzsj0928/ReadmeContents/master/liquid/luci-theme-liquid_pc-mainpage-light.png) + + +- 移动端 + +![移动端-暗黑模式](https://raw.githubusercontent.com/zzsj0928/ReadmeContents/master/liquid/luci-theme-liquid_mobile-dark.jpg) +![移动端-明亮模式](https://raw.githubusercontent.com/zzsj0928/ReadmeContents/master/liquid/luci-theme-liquid_mobile-light.jpg) + +## Credits + +- Wallpapers: macOS Monterey (Bright/Dark), from [xylz0928/luci-mod][luci-mod] `Background/`. +- Network status SVG icons: from [xylz0928/luci-mod][luci-mod] `immortalwrt-24.10` branch `feeds/luci/modules/luci-base/htdocs/luci-static/resources/icons/`. +- dashboard module icons (router / internet / wireless / devices): from official OpenWrt [luci-mod-dashboard](https://github.com/openwrt/luci/tree/master/modules/luci-mod-dashboard). +- Menu rendering logic references [luci-theme-openwrt-2020](https://github.com/openwrt/luci/tree/master/themes/luci-theme-openwrt-2020), lock screen view references [luci-theme-bootstrap](https://github.com/openwrt/luci/tree/master/themes/luci-theme-bootstrap) (Apache-2.0). + +## 🍡 Buy me a cup of TOKEN 😜 +**Appreciate Your Like!** +![Awards](https://raw.githubusercontent.com/zzsj0928/ReadmeContents/master/general/donate-zed.jpg) + +## License + +Apache-2.0 diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/cascade.css b/luci-theme-liquid/htdocs/luci-static/liquid/cascade.css new file mode 100644 index 000000000..6064d2477 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/cascade.css @@ -0,0 +1,4874 @@ +/*! + * LuCI Liquid Glass Theme — cascade.css + * + * A macOS-style liquid glass theme for LuCI >= 23 (23.05 / master). + * Light / dark / auto modes are driven by + * (set by main.js or the inline script in header.ut). + * + * Copyright 2026 然后七年 <69092025+zzsj0928@users.noreply.github.com> + * Licensed to the public under the Apache License 2.0. + */ + +/* ========================================================================== + 1. Design tokens + ========================================================================== */ + +:root { + /* -- LuCI compatible palette (light) -- */ + --background-color-high: #f4f6fa; + --background-color-medium: #e9edf4; + --background-color-low: #dfe5ef; + + --text-color-highest: #10151d; + --text-color-high: #1d2633; + --text-color-medium: #4c5868; + --text-color-low: #8a95a6; + + --border-color-high: rgba(23, 34, 52, 0.20); + --border-color-medium: rgba(23, 34, 52, 0.13); + --border-color-low: rgba(23, 34, 52, 0.07); + + --primary-color-high: #2f7fe0; + --primary-color-medium: #1f6fc8; + --primary-color-low: #0f56a8; + --on-primary-color: #ffffff; + + --error-color-high-rgb: 224, 58, 58; + --error-color-high: rgb(var(--error-color-high-rgb)); + --error-color-medium: #d23c3c; + --error-color-low: #b52a2a; + --on-error-color: #ffffff; + + --success-color-high-rgb: 16, 166, 96; + --success-color-high: rgb(var(--success-color-high-rgb)); + --success-color-medium: #0d9a58; + --success-color-low: #0a8048; + --on-success-color: #ffffff; + + --warn-color-high: #e0a013; + --warn-color-medium: #d99414; + --warn-color-low: #c98716; + --on-warn-color: #3a2c00; + + --disabled-opacity: .65; + + color-scheme: light; + + --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + "Helvetica Neue", Arial, ui-sans-serif, system-ui, sans-serif; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, + "Liberation Mono", monospace; + + /* -- liquid glass extras (light) -- */ + --glass-blur: 26px; + --glass-sat: 1.6; + --glass-radius: 18px; + --glass-radius-lg: 24px; + --glass-opacity: 1; + --glass-bg: rgba(255, 255, 255, calc(0.42 * var(--glass-opacity))); + --glass-bg-strong: rgba(255, 255, 255, calc(0.66 * var(--glass-opacity))); + /* 亮色 hover 背景用浅主题色 tint(纯白 hover 会发白、看不清字) */ + --glass-bg-hover: color-mix(in srgb, color-mix(in srgb, var(--primary-color-high) 30%, #ffffff) calc(100% * var(--glass-opacity)), transparent); + /* picker 选项高亮:沿用 glass-bg-hover 逻辑(亮底对比度已足够) */ + --picker-option-hover: var(--glass-bg-hover); + --picker-option-checked: var(--glass-bg-hover); + /* 表格隔行:不透明浅色(半透明玻璃隔行会让图标文字显得半透, + 误以为接口未连接) */ + --table-stripe: #f4f6f9; + --tab-active-text-color: var(--text-color-highest); + --glass-border: rgba(255, 255, 255, calc(0.8 * var(--glass-opacity))); + --glass-border-dark: rgba(23, 34, 52, calc(0.09 * var(--glass-opacity))); + --glass-inset: rgba(255, 255, 255, calc(0.95 * var(--glass-opacity))); + --glass-shadow: 0 22px 54px rgba(31, 45, 70, 0.16), 0 2px 8px rgba(31, 45, 70, 0.06); + --glass-shadow-soft: 0 8px 24px rgba(31, 45, 70, 0.10); + + --menu-bg: rgba(255, 255, 255, calc(0.36 * var(--glass-opacity))); + --field-bg: rgba(255, 255, 255, calc(0.56 * var(--glass-opacity))); + --track-bg: rgba(23, 34, 52, calc(0.10 * var(--glass-opacity))); + + --accent-grad: linear-gradient(135deg, #5aa8f7 0%, #2f7fe0 48%, #1a63c4 100%); + --accent-glow: rgba(47, 127, 224, 0.38); + --lock-card-bg: rgba(255, 255, 255, calc(0.5 * var(--glass-opacity))); + --indicator-bg: var(--accent-glass-soft); + --indicator-border: color-mix(in srgb, var(--primary-color-high) 48%, transparent); + --indicator-shadow: 0 4px 16px color-mix(in srgb, var(--primary-color-high) 30%, transparent), inset 0 1px 0 rgba(255, 255, 255, 0.9); + --accent-glass: linear-gradient(135deg, rgba(90, 168, 247, 0.32), rgba(255, 255, 255, 0.62)); + --accent-glass-soft: linear-gradient(135deg, rgba(90, 168, 247, 0.26), rgba(255, 255, 255, 0.72)); + /* 移动端 chrome 条(菜单栏/面包屑/底部 foot/菜单抽屉)去掉 + backdrop-filter 后的等价不透明色:分别是 --glass-bg-strong、 + --menu-bg 叠 --background-color-medium 的合成值(与 meta + theme-color 一致,main.js syncThemeColor 同公式动态跟随)。 + 安卓 Chrome 的地址栏收缩动画与 sticky/fixed 元素的 + backdrop-filter 重采样冲突(iOS 不受影响),移动端禁模糊改实底 */ + --glass-chrome-solid: #f8f9fb; + --glass-menu-solid: #f1f3f8; + --body-grad: + radial-gradient(1100px 750px at 85% -10%, rgba(96, 165, 250, 0.38), transparent 60%), + radial-gradient(950px 700px at -10% 30%, rgba(167, 139, 250, 0.30), transparent 55%), + radial-gradient(900px 620px at 55% 118%, rgba(45, 212, 191, 0.24), transparent 55%), + linear-gradient(160deg, #eef2f9 0%, #e7edf6 50%, #e2e9f5 100%); +} + +:root[data-darkmode="true"] { + /* -- LuCI compatible palette (dark) -- */ + --background-color-high: #10141c; + --background-color-medium: #161c27; + --background-color-low: #1d2532; + + --text-color-highest: #f1f4f9; + --text-color-high: #d9dfe9; + --text-color-medium: #96a2b6; + --text-color-low: #66728a; + + --border-color-high: rgba(255, 255, 255, 0.20); + --border-color-medium: rgba(255, 255, 255, 0.13); + --border-color-low: rgba(255, 255, 255, 0.07); + + --primary-color-high: #5da3f0; + --primary-color-medium: #4790e6; + --primary-color-low: #2f74cc; + --on-primary-color: #0b1220; + + --error-color-high-rgb: 226, 98, 98; + --error-color-high: rgb(var(--error-color-high-rgb)); + --error-color-medium: #cf5f5f; + --error-color-low: #b34a4a; + --on-error-color: #0b1220; + + --success-color-high-rgb: 46, 185, 118; + --success-color-high: rgb(var(--success-color-high-rgb)); + --success-color-medium: #38a86f; + --success-color-low: #2f8f5d; + --on-success-color: #0b1220; + + --warn-color-high: #d9a33a; + --warn-color-medium: #c99430; + --warn-color-low: #b8842a; + --on-warn-color: #221a05; + + --disabled-opacity: .45; + + color-scheme: dark; + + --glass-bg: rgba(24, 32, 48, calc(0.4 * var(--glass-opacity))); + --glass-bg-strong: rgba(22, 29, 44, calc(0.74 * var(--glass-opacity))); + --glass-bg-hover: rgba(42, 54, 78, calc(0.55 * var(--glass-opacity))); + /* picker 选项高亮:暗底需要更亮的选中/悬停色,否则看不清 */ + --picker-option-hover: rgba(70, 100, 160, calc(0.55 * var(--glass-opacity))); + --picker-option-checked: rgba(60, 90, 150, calc(0.45 * var(--glass-opacity))); + --table-stripe: #1b2330; + --tab-active-text-color: var(--text-color-highest); + --glass-border: rgba(255, 255, 255, calc(0.16 * var(--glass-opacity))); + --glass-border-dark: rgba(0, 0, 0, calc(0.42 * var(--glass-opacity))); + --glass-inset: rgba(255, 255, 255, calc(0.10 * var(--glass-opacity))); + --glass-shadow: 0 26px 64px rgba(0, 0, 0, 0.6), 0 2px 10px rgba(0, 0, 0, 0.32); + --glass-shadow-soft: 0 10px 30px rgba(0, 0, 0, 0.5); + + --menu-bg: rgba(12, 17, 27, calc(0.52 * var(--glass-opacity))); + --field-bg: rgba(255, 255, 255, calc(0.07 * var(--glass-opacity))); + --track-bg: rgba(255, 255, 255, calc(0.10 * var(--glass-opacity))); + + /* 移动端 chrome 条实色等价(暗):见亮色同名变量注释 */ + --glass-chrome-solid: #151c2b; + --glass-menu-solid: #111621; + + --accent-grad: linear-gradient(135deg, #7cbcff 0%, #5da3f0 48%, #2f74cc 100%); + --accent-glow: rgba(93, 163, 240, 0.4); + --lock-card-bg: rgba(13, 18, 28, calc(0.55 * var(--glass-opacity))); + --indicator-bg: var(--accent-glass-soft); + --indicator-border: rgba(255, 255, 255, 0.18); + --indicator-shadow: 0 4px 16px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.1); + --accent-glass: linear-gradient(135deg, rgba(93, 163, 240, 0.30), rgba(46, 58, 84, 0.5)); + --accent-glass-soft: linear-gradient(135deg, rgba(93, 163, 240, 0.24), rgba(46, 58, 84, 0.45)); + + + --body-grad: + radial-gradient(1100px 750px at 85% -10%, rgba(47, 102, 196, 0.42), transparent 60%), + radial-gradient(950px 700px at -10% 30%, rgba(109, 70, 220, 0.34), transparent 55%), + radial-gradient(900px 620px at 55% 118%, rgba(13, 148, 136, 0.26), transparent 55%), + linear-gradient(160deg, #0d1220 0%, #0a0f18 50%, #060a12 100%); +} +/* ========================================================================== + 1b. Accent color themes (data-accent on ) + ========================================================================== */ + +:root[data-accent="magenta"] { + --primary-color-high: #d63384; + --primary-color-medium: #bd2c72; + --primary-color-low: #9c1f5e; + --accent-grad: linear-gradient(135deg, #f270b5 0%, #d63384 48%, #a61e6a 100%); + --accent-glow: rgba(214, 51, 132, 0.38); + --accent-glass: linear-gradient(135deg, rgba(214, 51, 132, 0.30), rgba(255, 255, 255, 0.62)); + --accent-glass-soft: linear-gradient(135deg, rgba(214, 51, 132, 0.24), rgba(255, 255, 255, 0.72)); +} + +:root[data-darkmode="true"][data-accent="magenta"] { + --primary-color-high: #e86fb0; + --primary-color-medium: #dc5aa0; + --primary-color-low: #c23e8f; + --accent-grad: linear-gradient(135deg, #f59ad0 0%, #e86fb0 48%, #c23e8f 100%); + --accent-glow: rgba(232, 111, 176, 0.4); + --accent-glass: linear-gradient(135deg, rgba(232, 111, 176, 0.28), rgba(46, 58, 84, 0.45)); + --accent-glass-soft: linear-gradient(135deg, rgba(232, 111, 176, 0.22), rgba(46, 58, 84, 0.4)); +} + +:root[data-accent="amber"] { + --primary-color-high: #e8940f; + --primary-color-medium: #d1810d; + --primary-color-low: #b06e0a; + --accent-grad: linear-gradient(135deg, #ffc04a 0%, #e8940f 48%, #c47100 100%); + --accent-glow: rgba(232, 148, 15, 0.38); + --accent-glass: linear-gradient(135deg, rgba(232, 148, 15, 0.30), rgba(255, 255, 255, 0.62)); + --accent-glass-soft: linear-gradient(135deg, rgba(232, 148, 15, 0.24), rgba(255, 255, 255, 0.72)); +} + +:root[data-darkmode="true"][data-accent="amber"] { + --primary-color-high: #f5b74a; + --primary-color-medium: #eda63a; + --primary-color-low: #d99414; + --accent-grad: linear-gradient(135deg, #ffd98a 0%, #f5b74a 48%, #d99414 100%); + --accent-glow: rgba(245, 183, 74, 0.4); + --accent-glass: linear-gradient(135deg, rgba(245, 183, 74, 0.28), rgba(46, 58, 84, 0.45)); + --accent-glass-soft: linear-gradient(135deg, rgba(245, 183, 74, 0.22), rgba(46, 58, 84, 0.4)); +} + +:root[data-accent="purple"] { + --primary-color-high: #8e44ad; + --primary-color-medium: #7c3a99; + --primary-color-low: #672f82; + --accent-grad: linear-gradient(135deg, #c084fc 0%, #8e44ad 48%, #6a2f8e 100%); + --accent-glow: rgba(142, 68, 173, 0.38); + --accent-glass: linear-gradient(135deg, rgba(142, 68, 173, 0.30), rgba(255, 255, 255, 0.62)); + --accent-glass-soft: linear-gradient(135deg, rgba(142, 68, 173, 0.24), rgba(255, 255, 255, 0.72)); +} + +:root[data-darkmode="true"][data-accent="purple"] { + --primary-color-high: #b483d8; + --primary-color-medium: #a470c8; + --primary-color-low: #8e44ad; + --accent-grad: linear-gradient(135deg, #d8b4fe 0%, #b483d8 48%, #8e44ad 100%); + --accent-glow: rgba(180, 131, 216, 0.4); + --accent-glass: linear-gradient(135deg, rgba(180, 131, 216, 0.28), rgba(46, 58, 84, 0.45)); + --accent-glass-soft: linear-gradient(135deg, rgba(180, 131, 216, 0.22), rgba(46, 58, 84, 0.4)); +} + +:root[data-accent="lime"] { + --primary-color-high: #7fb52a; + --primary-color-medium: #709e24; + --primary-color-low: #5f8a1e; + --accent-grad: linear-gradient(135deg, #c0e35c 0%, #7fb52a 48%, #5f8a1e 100%); + --accent-glow: rgba(127, 181, 42, 0.38); + --accent-glass: linear-gradient(135deg, rgba(127, 181, 42, 0.30), rgba(255, 255, 255, 0.62)); + --accent-glass-soft: linear-gradient(135deg, rgba(127, 181, 42, 0.24), rgba(255, 255, 255, 0.72)); +} + +:root[data-darkmode="true"][data-accent="lime"] { + --primary-color-high: #a3d04a; + --primary-color-medium: #93c23c; + --primary-color-low: #7fb52a; + --accent-grad: linear-gradient(135deg, #d8f08a 0%, #a3d04a 48%, #7fb52a 100%); + --accent-glow: rgba(163, 208, 74, 0.4); + --accent-glass: linear-gradient(135deg, rgba(163, 208, 74, 0.28), rgba(46, 58, 84, 0.45)); + --accent-glass-soft: linear-gradient(135deg, rgba(163, 208, 74, 0.22), rgba(46, 58, 84, 0.4)); +} + +/* 自定义主题色(data-accent="custom"):主色来自 --accent-custom(JS 从 + uci accent_custom / 输入框设置),派生色变量由 main.js 的 + applyCustomAccent() 生成,此处给出默认蓝 fallback(JS 未跑时防闪烁) */ +:root[data-accent="custom"] { + --primary-color-high: var(--accent-custom, #2f7fe0); + --primary-color-medium: var(--accent-custom-medium, #286cbe); + --primary-color-low: var(--accent-custom-low, #205698); + --accent-grad: var(--accent-custom-grad, linear-gradient(135deg, #639fe8 0%, #2f7fe0 48%, #1c4c86 100%)); + --accent-glow: var(--accent-custom-glow, rgba(47, 127, 224, 0.38)); + --accent-glass: var(--accent-custom-glass, linear-gradient(135deg, rgba(47, 127, 224, 0.30), rgba(255, 255, 255, 0.62))); + --accent-glass-soft: var(--accent-custom-glass-soft, linear-gradient(135deg, rgba(47, 127, 224, 0.24), rgba(255, 255, 255, 0.72))); +} + +:root[data-darkmode="true"][data-accent="custom"] { + --primary-color-high: var(--accent-custom-dark-high, #6da5e9); + --primary-color-medium: var(--accent-custom-dark-medium, #488ee4); + --primary-color-low: var(--accent-custom-dark-low, #2f7fe0); + --accent-grad: var(--accent-custom-dark-grad, linear-gradient(135deg, #9cc8f7 0%, #6da5e9 48%, #488ee4 100%)); + --accent-glow: var(--accent-custom-dark-glow, rgba(109, 165, 233, 0.42)); + --accent-glass: var(--accent-custom-dark-glass, linear-gradient(135deg, rgba(109, 165, 233, 0.28), rgba(46, 58, 84, 0.45))); + --accent-glass-soft: var(--accent-custom-dark-glass-soft, linear-gradient(135deg, rgba(109, 165, 233, 0.22), rgba(46, 58, 84, 0.40))); +} + + +/* ========================================================================== + 2. Reset & base + ========================================================================== */ + +*, *::before, *::after { + box-sizing: border-box; +} + +html { + font-size: 100%; + min-height: 100%; +} + +body { + font-family: var(--font-sans); + font-size: 14px; + line-height: 1.55; + color: var(--text-color-high); + background: var(--background-color-medium) var(--body-grad) fixed; + background-attachment: fixed; + height: 100%; + min-height: 100vh; + margin: 0; + overflow: hidden; + -webkit-font-smoothing: antialiased; +} + +a { + color: var(--primary-color-high); +} + +a:hover { + text-decoration: underline; +} + +ul, ol { + list-style: none; + margin: 0; + padding: 0; +} + +h1, h2, h3, h4 { + font-weight: 700; + line-height: 1.3; + margin: 0 0 .5em 0; +} + +#view h2 { + font-size: 17px; + color: var(--text-color-highest); + margin: 6px 0 10px; +} + +hr { + border: 0; + border-top: 1px solid var(--border-color-medium); + margin: .75em 0; +} + +code, pre, kbd { + font-family: var(--font-mono); +} + +img { + max-width: 100%; +} + +/* scrollbars */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--border-color-medium); + border: 2px solid transparent; + border-radius: 999px; + background-clip: content-box; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--border-color-high); + border: 2px solid transparent; + background-clip: content-box; +} + +/* ========================================================================== + 3. Top bar + ========================================================================== */ + +#menubar { + position: sticky; + top: 0; + z-index: 900; + display: flex; + align-items: center; + gap: 12px; + min-height: 56px; + padding: 0 18px; + background: var(--glass-bg-strong); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + border-bottom: 1px solid var(--glass-border-dark); + box-shadow: 0 1px 0 var(--glass-inset), 0 6px 24px rgba(0, 0, 0, 0.06); +} + +#menubar .navigation { + display: none; + align-items: center; + justify-content: center; + width: 38px; + height: 38px; + margin: 0; + flex-shrink: 0; + border-radius: 12px; + cursor: pointer; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + transition: background .15s ease, transform .15s ease; +} + +#menubar .navigation:hover { + background: var(--glass-bg-hover); +} + +#menubar .navigation a { + display: block; + position: relative; + width: 18px; + height: 14px; + text-indent: -9999px; + overflow: hidden; +} + +#menubar .navigation a::before, +#menubar .navigation a::after { + content: ""; + position: absolute; + left: 0; + width: 18px; + height: 2px; + border-radius: 2px; + background: var(--text-color-high); + transition: top .15s ease, transform .15s ease; +} + +#menubar .navigation a::before { + top: 0; + box-shadow: 0 6px 0 var(--text-color-high); +} + +#menubar .navigation a::after { + top: 12px; +} + +#menubar .navigation.active a::before { + top: 6px; + transform: rotate(45deg); + box-shadow: none; +} + +#menubar .navigation.active a::after { + top: 6px; + transform: rotate(-45deg); +} + +#menubar .hostname { + font-size: 16px; + font-weight: 700; + letter-spacing: .2px; + white-space: nowrap; +} + +#menubar .hostname a { + color: var(--text-color-highest); + text-decoration: none; +} + +/* 顶栏 logo(内联 SVG,跟随主题色 + 玻璃水滴感) */ +#menubar .hostname a .liquid-logo { + display: inline-block; + width: 22px; + height: 22px; + margin: 0 8px -5px 0; + vertical-align: baseline; + /* 与登录页 logo 同款光晕效果:三层 drop-shadow 沿水滴轮廓 + 高光线 */ + filter: drop-shadow(0 0 3px color-mix(in srgb, var(--primary-color-high) 35%, transparent)) + drop-shadow(0 1px 6px color-mix(in srgb, var(--primary-color-high) 25%, transparent)) + drop-shadow(0 0 12px color-mix(in srgb, var(--primary-color-high) 12%, transparent)) + drop-shadow(0 0.5px 0 rgba(255, 255, 255, 0.7)); + transition: filter .25s ease; +} +#menubar .hostname a .liquid-logo:hover { + filter: drop-shadow(0 0 5px color-mix(in srgb, var(--primary-color-high) 50%, transparent)) + drop-shadow(0 2px 10px color-mix(in srgb, var(--primary-color-high) 35%, transparent)) + drop-shadow(0 0 18px color-mix(in srgb, var(--primary-color-high) 18%, transparent)) + drop-shadow(0 1px 0 rgba(255, 255, 255, 0.9)); +} + +#menubar .distversion { + color: var(--text-color-low); + font-size: 12px; + white-space: nowrap; +} + +#menubar #indicators { + margin-left: auto; + display: flex; + align-items: center; + gap: 4px; +} + +header [data-indicator], +#menubar [data-indicator] { + padding: 5px 10px; + font-size: 10.5px; + font-weight: 700; + letter-spacing: .4px; + text-transform: uppercase; + color: var(--text-color-medium); + /* 淡化半透明玻璃质感(与二级菜单选中项同源) */ + background: color-mix(in srgb, var(--accent-glass-soft) 55%, transparent); + border: 1px solid color-mix(in srgb, var(--primary-color-high) 22%, transparent); + border-radius: 999px; + white-space: nowrap; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5); + -webkit-backdrop-filter: blur(8px); + backdrop-filter: blur(8px); + transition: background .18s ease, color .18s ease, border-color .18s ease; +} + +header [data-indicator][data-clickable], +#menubar [data-indicator][data-clickable] { + cursor: pointer; +} + +header [data-indicator][data-clickable]:hover, +#menubar [data-indicator][data-clickable]:hover { + background: var(--accent-glass-soft); + border-color: color-mix(in srgb, var(--primary-color-high) 38%, transparent); + color: var(--text-color-highest); +} + +header [data-indicator][data-style="active"], +#menubar [data-indicator][data-style="active"] { + color: var(--text-color-highest); + background: var(--accent-glass-soft); + border-color: color-mix(in srgb, var(--primary-color-high) 38%, transparent); + box-shadow: 0 2px 10px color-mix(in srgb, var(--primary-color-high) 20%, transparent), + inset 0 1px 0 rgba(255, 255, 255, 0.8); +} + +header [data-indicator][data-style="inactive"], +#menubar [data-indicator][data-style="inactive"] { + opacity: .7; +} + +/* ========================================================================== + 4. Light / dark / auto mode switch + ========================================================================== */ + +.liquid-mode-switch { + display: inline-flex; + align-items: center; + gap: 2px; + margin-left: 4px; + padding: 3px; + border-radius: 999px; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + box-shadow: 0 4px 14px rgba(31, 45, 70, 0.16), + inset 0 1px 0 rgba(255, 255, 255, 0.6); + vertical-align: middle; +} + +.liquid-mode-switch .liquid-mode-btn { + display: flex; + align-items: center; + justify-content: center; + flex: none; + width: 28px; + height: 28px; + padding: 0; + border: 0; + border-radius: 999px; + background: transparent; + color: var(--text-color-medium); + cursor: pointer; + transition: background .15s ease, color .15s ease, box-shadow .15s ease; +} + +.liquid-mode-switch .liquid-mode-btn:hover { + color: var(--text-color-highest); +} + +.liquid-mode-switch .liquid-mode-btn.active { + color: var(--on-primary-color); + background: var(--accent-grad); + box-shadow: 0 3px 10px var(--accent-glow); +} + +.liquid-mode-switch .liquid-mode-btn img { + width: 16px; + height: 16px; + pointer-events: none; +} + +/* 暗黑模式:模式开关图标(外链 svg 的 currentColor 在 里不继承 + CSS color,解析为默认黑色)反色为浅色,避免深色背景下黑图标 */ +:root[data-darkmode="true"] .liquid-mode-btn img { + filter: invert(1); +} + +.liquid-mode-switch-fixed { + position: fixed; + top: 16px; + right: 16px; + z-index: 2001; + margin: 0; +} + +/* 顶栏模式开关(刷新指示区左侧) */ +#menubar > .liquid-mode-switch { + margin: 0 12px 0 0; + flex-shrink: 0; +} + +/* 菜单栏顶部颜色胶囊(水平居中)。胶囊宽度 min(216px, 100%)(侧栏 + 胶囊宽度 = 内容宽度(max-content):8 个圆点(Bing + 5 色 + 自定义 + 占位) + 永远排在同一行,宽度随内容自适应,任何侧栏宽度都不会换行、不会 + 超出胶囊;margin auto 居中保证左右两侧间距一致;圆点 flex:none + 固定 18px 正圆不压缩、不缩小。 */ +#mainmenu > .liquid-color-switch { + display: flex; + justify-content: center; + align-items: center; + flex-wrap: nowrap; + width: calc(100% + 8px); + max-width: calc(100% + 8px); + margin: 0 -4px 28px; + flex-shrink: 0; + box-sizing: border-box; +} + +/* accent color switch */ +.liquid-color-switch { + position: relative; + height: 35px; + box-sizing: border-box; + display: inline-flex; + align-items: center; + gap: 0; + margin-left: 6px; + padding: 0 18px; + border-radius: 999px; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + box-shadow: 0 4px 14px rgba(31, 45, 70, 0.16), + inset 0 1px 0 rgba(255, 255, 255, 0.6); + vertical-align: middle; +} + +.liquid-color-btn { + flex: none; + width: 18px; + height: 18px; + padding: 0; + border: 2px solid rgba(255, 255, 255, 0.85); + border-radius: 50%; + cursor: pointer; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.28); + transition: transform .15s ease, box-shadow .15s ease; +} + +/* Bing 壁纸按钮(五色之前):点亮 = 启用 Bing 每日壁纸。 + 方形外框(跟随 b logo 的圆角方形图标 rx=5),选中时白色描边也是方形 */ +.liquid-bing-btn { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + padding: 0; + border-radius: 5px; + border: 2px solid transparent; + background: transparent; + cursor: pointer; + transition: transform .15s ease, box-shadow .15s ease, opacity .15s ease; + opacity: .55; +} + +.liquid-bing-btn:hover { + transform: scale(1.15); + opacity: 1; +} + +/* 启用时只保留一层白色方形描边(border)。 + 不能有 box-shadow 白环:按钮同时带 .liquid-color-btn 类, + 其 .active 的 2px 白环会叠成第二层圈,这里用 !important 压掉 */ +.liquid-bing-btn.active { + transform: scale(1.15); + border-color: rgba(255, 255, 255, 0.85); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3) !important; + opacity: 1; +} + +.liquid-color-btn:hover { + transform: scale(1.15); +} + +.liquid-color-btn.active { + transform: scale(1.15); + box-shadow: 0 0 0 2px var(--text-color-high), 0 2px 6px rgba(0, 0, 0, 0.3); +} + +.liquid-color-switch-fixed { + position: fixed; + top: 60px; + right: 16px; + z-index: 2001; + margin: 0; +} + +/* 自定义主题色按钮(5 色之后):彩虹圆点;激活时显示当前自定义色 */ +.liquid-custom-btn { + background: conic-gradient(#f66, #fc3, #6c6, #3cf, #96f, #f66); + opacity: .85; +} + +/* 选中时:内部显示自定义色(第一层,clip 到 padding-box), + 描边(2px 边框区)为彩虹渐变(第二层,clip 到 border-box), + 一眼识别"这是自定义颜色"。不能用 background 简写(color 不能放 + 在非最后层的 box 关键字后,整条会被浏览器丢弃) */ +.liquid-custom-btn.active { + background-image: + linear-gradient(var(--accent-custom, #2f7fe0), var(--accent-custom, #2f7fe0)), + conic-gradient(#f66, #fc3, #6c6, #3cf, #96f, #f66); + background-clip: padding-box, border-box; + background-origin: padding-box, border-box; + border: 3px solid transparent; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); + opacity: 1; +} + +/* 菜单搜索按钮:与颜色圆点完全同步的交互设计(白描边、hover + 放大、启用黑圈),仅形状为圆角方框 + 浅灰底 + 白色放大镜。 + 所有状态显式声明,覆盖 .liquid-color-btn 的继承干扰 */ +.liquid-search-btn { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + padding: 0; + background: rgba(148, 163, 184, 0.55); /* 浅灰底(明亮模式) */ + border-radius: 5px; /* 方框 */ + border: 2px solid rgba(255, 255, 255, 0.85); /* 与其他圆点一致白描边 */ + color: #ffffff !important; /* 白色放大镜,任何状态不变黑 */ + cursor: pointer; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.28); + opacity: .95; + transition: transform .15s ease, box-shadow .15s ease, opacity .15s ease; +} +.liquid-search-btn svg { + width: 12px; + height: 12px; + display: block; +} +.liquid-search-btn:hover { + transform: scale(1.15); /* 描边一起放大,同其他圆点 */ + background: rgba(148, 163, 184, 0.65); + opacity: 1; +} +.liquid-search-btn.active { + transform: scale(1.15); + background: rgba(148, 163, 184, 0.65); + box-shadow: 0 0 0 2px var(--text-color-high), /* 黑圈同其他圆点 */ + 0 2px 6px rgba(0, 0, 0, 0.3); + opacity: 1; +} + +/* 暗黑模式:白色方框底(用户指定),深色放大镜 */ +:root[data-darkmode="true"] .liquid-search-btn { + background: rgba(255, 255, 255, 0.92); + color: #334155 !important; + border-color: rgba(255, 255, 255, 0.85); +} +:root[data-darkmode="true"] .liquid-search-btn:hover { + background: #ffffff; +} +:root[data-darkmode="true"] .liquid-search-btn.active { + background: #ffffff; + box-shadow: 0 0 0 2px var(--text-color-high), + 0 2px 6px rgba(0, 0, 0, 0.3); +} + +/* ── 菜单搜索框(抽屉式下推,圆角矩形)────────────── + 与颜色胶囊同源玻璃设计,但圆角矩形(10px 非胶囊), + 插在颜色胶囊下方,菜单被往下推(非悬浮)。 + 放大镜/清除图标绝对定位在输入框内部(不隔离) */ +.liquid-menu-search { + display: flex; + position: relative; + align-items: center; + margin: 0 auto 12px; + height: 0; /* 默认收起:0 高不占位 */ + padding: 0; + opacity: 0; + overflow: hidden; /* 圆角裁剪子元素(input 撑满后不露直角) */ + border-radius: 10px; + border-width: 0; /* 收起时无边框 */ + background: var(--glass-bg); + border: 1px solid var(--glass-border); + box-shadow: 0 4px 14px rgba(31, 45, 70, 0.16), + inset 0 1px 0 rgba(255, 255, 255, 0.6); + color: var(--text-color-medium); + -webkit-backdrop-filter: blur(10px) saturate(var(--glass-sat)); + backdrop-filter: blur(10px) saturate(var(--glass-sat)); + box-sizing: border-box; + transition: height .16s ease, opacity .16s ease, padding .16s ease, margin .16s ease; +} +.liquid-menu-search.open { + height: 34px; + padding: 0; + opacity: 1; + border-width: 1px; + animation: none; +} + +/* 收回:过渡动画由 height/opacity transition 处理(.open 移除即触发) */ + +@keyframes liquid-search-drop { + from { opacity: 0; transform: translateY(-8px); } + to { opacity: 1; transform: translateY(0); } +} + +.liquid-menu-search-icon { + position: absolute; + left: 8px; + top: 50%; + transform: translateY(-50%); + display: inline-flex; + color: var(--text-color-medium); + pointer-events: none; +} + +.liquid-menu-search-input { + flex: 1; + width: 100%; + height: 100%; /* 撑满容器高度,避免露出容器/页面背景色差 */ + min-width: 0; + /* 用 !important 压掉 LuCI 全局 input[type="text"] 的 padding/背景/边框: + 28px = 图标(8px 左偏移+12px 宽) + 8px 间距,文字贴图标右侧 */ + padding: 4px 30px 4px 28px !important; + border: none !important; + outline: none; + background: transparent !important; /* 压掉 --field-bg 底色,融入容器 */ + /* 圆角跟随容器(容器 10px + 1px 边框 → input 内缩用 9px), + 避免撑满后直角盖掉容器圆角 */ + border-radius: 9px !important; + display: block; + color: var(--text-color-high); + font-size: 12px; + box-sizing: border-box; +} +/* 聚焦时:恢复主题色高亮边框(用户要求),但保持透明底不破坏玻璃。 + 用容器 :focus-within 让整个搜索框显示主题色描边 + 淡光晕 */ +.liquid-menu-search:focus-within { + border-color: var(--primary-color-high) !important; + box-shadow: 0 0 0 3px var(--accent-glow), 0 4px 14px rgba(31, 45, 70, 0.16) !important; +} +.liquid-menu-search-input:focus { + background: transparent !important; + border-color: transparent !important; + box-shadow: none !important; +} +/* 浏览器 autofill 会强加黄/白底,压掉保持玻璃透明 */ +.liquid-menu-search-input:-webkit-autofill { + -webkit-box-shadow: 0 0 0 1000px transparent inset !important; + -webkit-text-fill-color: var(--text-color-high) !important; + transition: background-color 9999s ease-out !important; +} +.liquid-menu-search-input::placeholder { + color: var(--text-color-medium); + opacity: .7; +} + +.liquid-menu-search-clear { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + width: 18px; + height: 18px; + padding: 0; + border: none; + border-radius: 50%; + background: rgba(148, 163, 184, 0.35); + color: var(--text-color-medium); + font-size: 13px; + line-height: 1; + cursor: pointer; + display: none; /* 默认隐藏,有输入时 JS 显示 */ + align-items: center; + justify-content: center; + opacity: .75; +} +.liquid-menu-search-clear:hover { + opacity: 1; +} + +/* 搜索过滤:不匹配的菜单项隐藏 */ +#mainmenu li.liquid-menu-hidden { + display: none !important; +} + +/* 自定义颜色输入面板(点击彩虹圆点下拉) */ +.liquid-custom-pop { + display: none; + position: absolute; + top: calc(100% + 8px); + right: 0; + z-index: 9999; + padding: 6px; + border-radius: 8px; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow-soft), inset 0 1px 0 var(--glass-inset); + -webkit-backdrop-filter: blur(var(--glass-blur)); + backdrop-filter: blur(var(--glass-blur)); + animation: liquid-pop-in .3s ease; + transform-origin: top right; +} + +@keyframes liquid-pop-in { + from { opacity: 0; transform: translateY(-8px) scale(.95); } + to { opacity: 1; transform: translateY(0) scale(1); } +} + +.liquid-custom-input { + width: 112px; + padding: 4px 8px; + font-size: 12px; + font-family: ui-monospace, SFMono-Regular, Consolas, monospace; + color: var(--text-color); + background: var(--input-bg, rgba(255, 255, 255, 0.7)); + border: 1px solid var(--glass-border); + border-radius: 6px; + outline: none; +} + +.liquid-custom-input:focus { + border-color: var(--primary-color-high); + box-shadow: 0 0 0 2px var(--accent-glow); +} + +/* 锁屏(无菜单栏,开关固定在右上角):输入面板向上弹出 */ +.liquid-color-switch-fixed .liquid-custom-pop { + top: auto; + bottom: calc(100% + 8px); + transform-origin: bottom right; +} + +/* ── 透明度滑杆(玻璃感,参考 iOS 26/macOS 26 slider)───────── */ +/* 绝对定位在胶囊下方,不参与胶囊 flex 布局,不撑爆胶囊 */ +.liquid-glass-slider-wrap { + position: absolute; + left: 0; + right: 0; + top: calc(100% + 4px); + display: flex; + align-items: center; + height: 16px; + pointer-events: none; + z-index: 2; + /* 内容流不参与胶囊 flex 布局,overflow: visible 确保 tick/bubble 可见 */ +} +/* 胶囊需要 overflow:visible 才能显示溢出的滑杆 */ +.liquid-color-switch { + overflow: visible; +} +.liquid-glass-slider { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 4px; + border-radius: 4px; + outline: none; + cursor: pointer; + pointer-events: auto; + /* 轨道:亮色用深色半透明,暗色用浅色半透明 */ + background: linear-gradient(to right, rgba(0,0,0,0.12), rgba(0,0,0,0.22)); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + border: none; + box-shadow: 0 0 0 1px rgba(0,0,0,0.08), 0 1px 4px rgba(0,0,0,0.08); +} +:root[data-darkmode="true"] .liquid-glass-slider { + background: linear-gradient(to right, rgba(255,255,255,0.06), rgba(255,255,255,0.16)); + box-shadow: 0 0 0 1px rgba(255,255,255,0.12), 0 1px 4px rgba(0,0,0,0.08); +} +.liquid-glass-slider::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + border-radius: 50%; + background: linear-gradient(135deg, rgba(255,255,255,0.92), rgba(255,255,255,0.68)); + border: 1px solid rgba(255, 255, 255, 0.95); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2), 0 0 0 2px rgba(255, 255, 255, 0.15); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + cursor: pointer; +} +.liquid-glass-slider::-moz-range-thumb { + width: 16px; + height: 16px; + border-radius: 50%; + background: linear-gradient(135deg, rgba(255,255,255,0.92), rgba(255,255,255,0.68)); + border: 1px solid rgba(255, 255, 255, 0.95); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2), 0 0 0 2px rgba(255, 255, 255, 0.15); + cursor: pointer; +} +.liquid-glass-slider::-webkit-slider-thumb:hover { + background: linear-gradient(135deg, rgba(255,255,255,1), rgba(255,255,255,0.88)); + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.25), 0 0 0 3px rgba(255, 255, 255, 0.2); +} +.liquid-glass-slider::-webkit-slider-thumb:active { + transform: scale(0.9); +} +/* 暗黑模式滑杆轨道稍微更亮 */ +:root[data-darkmode="true"] .liquid-glass-slider { + background: linear-gradient(to right, rgba(255,255,255,0.06), rgba(255,255,255,0.16)); + border-color: rgba(255, 255, 255, 0.12); +} +/* 默认值竖线标记:绝对定位在轨道上方,垂直居中,百分比对齐 thumb */ +.liquid-glass-slider-tick { + position: absolute; + left: 0; + top: 0; + bottom: 0; + display: flex; + align-items: center; + pointer-events: auto; + cursor: pointer; + transition: opacity .15s ease; +} +/* tick 内部的竖线指示器(垂直居中由父 flex 保证) */ +.liquid-glass-slider-tick::before { + content: ''; + display: block; + width: 4px; + height: 14px; + flex-shrink: 0; + background: rgba(90, 95, 110, 0.85); + border: 1px solid rgba(255, 255, 255, 0.6); + box-shadow: 0 0 2px rgba(0,0,0,0.2); + border-radius: 2px; +} +.liquid-glass-slider-tick:hover::before { + background: rgba(60, 65, 80, 1); + border-color: rgba(255, 255, 255, 0.9); +} +/* 侧栏菜单里的胶囊下方不需要下移(胶囊本身就是菜单项,位置已定) */ +#mainmenu > .liquid-color-switch > .liquid-glass-slider-wrap { + top: calc(100% + 2px); +} + +/* 浮动数值气泡(拖动时跟随 thumb 显示当前值) */ +.liquid-glass-slider-bubble { + position: absolute; + bottom: calc(100% + 6px); + transform: translateX(-50%); + padding: 3px 7px; + border-radius: 6px; + font-size: 11px; + font-weight: 600; + line-height: 1; + color: var(--text-color-highest); + background: var(--glass-bg-strong); + border: 1px solid var(--glass-border); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + box-shadow: 0 2px 8px rgba(0,0,0,0.15); + pointer-events: none; + white-space: nowrap; +} +.liquid-glass-slider-bubble::after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + margin-left: -4px; + border: 4px solid transparent; + border-top-color: var(--glass-border); +} + +/* 登录后暂存提示 toast */ +.liquid-pending-toast { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%) translateY(20px); + opacity: 0; + transition: opacity .3s ease, transform .3s ease; + z-index: 9999; + display: inline-flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 16px; + border-radius: 12px; + background: var(--glass-bg-strong); + border: 1px solid var(--glass-border); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + box-shadow: 0 8px 32px rgba(0,0,0,0.2); + pointer-events: auto; + max-width: 75vw; +} +.liquid-pending-toast.show { + opacity: 1; + transform: translateX(-50%) translateY(0); +} +.liquid-pending-toast-text { + font-size: 13px; + color: var(--text-color-high); + white-space: nowrap; + text-align: center; +} +.liquid-pending-toast-countdown { + font-size: 12px; + font-weight: 600; + color: var(--accent-glow); + font-variant-numeric: tabular-nums; + min-width: 28px; + text-align: center; +} +.liquid-pending-toast-buttons { + display: flex; + gap: 8px; +} +.liquid-pending-toast-btn { + flex-shrink: 0; + padding: 5px 14px; + border: none; + border-radius: 8px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + transition: box-shadow .15s ease, background .15s ease; +} +.liquid-pending-toast-btn[data-action="apply"] { + background: var(--accent-grad); + color: var(--on-primary-color, #fff); + box-shadow: 0 2px 8px var(--accent-glow); +} +.liquid-pending-toast-btn[data-action="apply"]:hover { + box-shadow: 0 4px 16px var(--accent-glow); +} +.liquid-pending-toast-btn[data-action="dismiss"] { + background: var(--glass-bg); + color: var(--text-color-medium); + border: 1px solid var(--glass-border); +} +.liquid-pending-toast-btn[data-action="dismiss"]:hover { + color: var(--text-color-high); +} + min-width: 80px; +} +.liquid-pending-toast-btn[data-action="apply"] { + background: var(--accent-grad); + color: var(--on-primary-color, #fff); + box-shadow: 0 2px 8px var(--accent-glow); +} +.liquid-pending-toast-btn[data-action="apply"]:hover { + box-shadow: 0 4px 16px var(--accent-glow); +} +.liquid-pending-toast-btn[data-action="dismiss"] { + background: var(--glass-bg); + color: var(--text-color-medium); + border: 1px solid var(--glass-border); +} +.liquid-pending-toast-btn[data-action="dismiss"]:hover { + color: var(--text-color-high); +} + +/* ========================================================================== + 5. Layout + ========================================================================== */ + +#maincontainer { + display: flex; + align-items: stretch; + width: 100%; + height: calc(100vh - 56px); + min-height: calc(100vh - 56px); + overflow-y: auto; +} + +#maincontent { + flex: 1 1 auto; + min-width: 0; + width: 100%; + margin-left: 236px; + padding: 6px 22px 96px; +} + +#view { + margin-top: 18px; +} + +.skiplink { + position: absolute; + left: -9999px; +} + +/* ========================================================================== + 6. Sidebar menu + ========================================================================== */ + +#mainmenu { + position: fixed; + top: 56px; + bottom: 0; + left: 0; + width: 236px; + flex-shrink: 0; + height: auto; + overflow-y: auto; + padding: 12px 10px 24px; + animation: liquid-slide-in .5s cubic-bezier(.22, .9, .3, 1); + background: var(--menu-bg); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + border-right: 1px solid var(--glass-border-dark); + transition: width .2s ease, transform .2s ease; +} + +#mainmenu ul.mainmenu { + display: block; +} + +#mainmenu ul.mainmenu.l1 > li > a { + display: flex; + align-items: center; + gap: 10px; + position: relative; + z-index: 3; + padding: 10px 14px; + margin-bottom: 3px; + border-radius: 13px; + border: 1px solid transparent; + font-weight: 600; + font-size: 13.5px; + color: var(--text-color-high); + text-decoration: none; + transition: background .16s ease, border-color .16s ease, + box-shadow .16s ease, color .16s ease; +} + +/* 悬停视觉由 .liquid-menu-indicator 滑块负责;这里仅文字变色 */ +#mainmenu ul.mainmenu.l1 > li > a:hover { + background: transparent; + border-color: transparent; + box-shadow: none; + color: var(--text-color-highest); +} + +/* menu item selected — 与追踪滑块同款的透明蓝玻璃,蓝色略深(不被 hover 覆盖) */ +#mainmenu ul.mainmenu.l1 > li.selected > a, +#mainmenu ul.mainmenu.l1 > li.selected.active > a, +#mainmenu ul.mainmenu.l1 > li.selected > a:hover, +#mainmenu ul.mainmenu.l1 > li.selected.active > a:hover { + background: var(--accent-glass); + border-color: color-mix(in srgb, var(--primary-color-high) 55%, transparent); + color: var(--primary-color-low); + box-shadow: 0 4px 16px color-mix(in srgb, var(--primary-color-high) 26%, transparent), + inset 0 1px 0 rgba(255, 255, 255, 0.9); + -webkit-backdrop-filter: blur(10px) saturate(var(--glass-sat)); + backdrop-filter: blur(10px) saturate(var(--glass-sat)); +} + +:root[data-darkmode="true"] #mainmenu ul.mainmenu.l1 > li.selected > a, +:root[data-darkmode="true"] #mainmenu ul.mainmenu.l1 > li.selected.active > a, +:root[data-darkmode="true"] #mainmenu ul.mainmenu.l1 > li.selected > a:hover, +:root[data-darkmode="true"] #mainmenu ul.mainmenu.l1 > li.selected.active > a:hover { + background: var(--accent-glass); + border-color: color-mix(in srgb, var(--primary-color-high) 48%, transparent); + color: #dcecff; + box-shadow: 0 4px 18px color-mix(in srgb, var(--primary-color-high) 38%, transparent), + inset 0 1px 0 rgba(255, 255, 255, 0.14); +} + +/* 展开态(点击展开的其他一级菜单)— 次级高亮 */ +#mainmenu ul.mainmenu.l1 > li.active > a, +#mainmenu ul.mainmenu.l1 > li.active > a:hover { + background: var(--glass-bg); + border-color: var(--glass-border); + color: var(--primary-color-high); + box-shadow: inset 0 0 0 1px var(--glass-border), + inset 0 1px 0 var(--glass-inset); +} + +#mainmenu ul.mainmenu.l1 > li.selected > a::before { + content: ""; + position: absolute; + left: -10px; + top: 22%; + height: 56%; + width: 4px; + border-radius: 0 4px 4px 0; + background: var(--accent-grad); + box-shadow: 0 0 12px var(--accent-glow); +} + +/* expandable submenu indicator */ +#mainmenu ul.mainmenu.l1 > li:has(> ul:not(:empty)) > a::after { + content: ""; + width: 12px; + height: 12px; + margin-left: auto; + background: url(svg/chevron-down.svg) center / contain no-repeat; + opacity: .65; + transition: transform .18s ease; + flex-shrink: 0; +} + +#mainmenu ul.mainmenu.l1 > li.active > a::after { + transform: rotate(180deg); +} + +/* submenu */ +#mainmenu li > ul, +#mainmenu li > ul.mainmenu { + display: none !important; + margin: 2px 0 6px 10px; + padding-left: 8px; + border-left: 1px solid var(--border-color-low); +} + +#mainmenu li.active > ul, +#mainmenu li.active > ul.mainmenu { + display: block !important; + animation: liquid-fade-in .22s ease; +} + +/* 二级菜单 hover 追踪滑块 */ +ul.mainmenu.l2 { + position: relative; +} + +.liquid-sub-indicator { + position: absolute; + left: 0; + right: 0; + top: 0; + height: 32px; + border-radius: 10px; + background: var(--indicator-bg); + border: 1px solid var(--indicator-border); + box-shadow: var(--indicator-shadow); + opacity: 0; + pointer-events: none; + z-index: 0; + transition: transform .25s cubic-bezier(.22, .8, .3, 1), + height .25s cubic-bezier(.22, .8, .3, 1), + opacity .18s ease; +} + +ul.mainmenu.l2.liquid-sub-hovering .liquid-sub-indicator { + opacity: 1; +} + +#mainmenu ul.mainmenu.l2 > li > a { + display: block; + position: relative; + z-index: 3; + padding: 8px 12px; + margin: 1px 0; + border-radius: 10px; + font-weight: 500; + font-size: 12.5px; + color: var(--text-color-medium); + text-decoration: none; + transition: background .15s ease, color .15s ease; +} + +#mainmenu ul.mainmenu.l2 > li > a:hover { + background: transparent; + color: var(--primary-color-high); +} + +/* 二级菜单选中项 — 淡蓝玻璃胶囊(亮色下可见,避免纯白难辨) */ +#mainmenu ul.mainmenu.l2 > li.selected > a, +#mainmenu ul.mainmenu.l2 > li.selected > a:hover { + background: var(--accent-glass-soft); + border-color: color-mix(in srgb, var(--primary-color-high) 38%, transparent); + color: var(--primary-color-low); + font-weight: 600; + box-shadow: 0 2px 10px color-mix(in srgb, var(--primary-color-high) 20%, transparent), + inset 0 1px 0 rgba(255, 255, 255, 0.8); +} + +:root[data-darkmode="true"] #mainmenu ul.mainmenu.l2 > li.selected > a, +:root[data-darkmode="true"] #mainmenu ul.mainmenu.l2 > li.selected > a:hover { + background: var(--accent-glass-soft); + border-color: color-mix(in srgb, var(--primary-color-high) 34%, transparent); + color: #dcecff; + box-shadow: 0 2px 10px color-mix(in srgb, var(--primary-color-high) 32%, transparent), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +#mainmenu:empty { + display: none; +} + +/* hover 追踪滑块 */ +.liquid-menu-indicator { + position: absolute; + left: 10px; + right: 10px; + top: 0; + height: 40px; + border-radius: 13px; + background: var(--indicator-bg); + border: 1px solid var(--indicator-border); + box-shadow: var(--indicator-shadow); + -webkit-backdrop-filter: blur(10px) saturate(var(--glass-sat)); + backdrop-filter: blur(10px) saturate(var(--glass-sat)); + opacity: 0; + pointer-events: none; + z-index: 0; + transition: transform .28s cubic-bezier(.22, .8, .3, 1), + height .28s cubic-bezier(.22, .8, .3, 1), + opacity .18s ease; +} + +#mainmenu.liquid-menu-hovering .liquid-menu-indicator { + opacity: 1; +} + + +/* ========================================================================== + 7. Mode menu (mobile breadcrumb) + ========================================================================== */ + +#modemenu { + position: sticky; + top: 56px; + z-index: 800; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 4px 10px; + padding: 10px 22px; + margin: 0; + background: var(--glass-bg); + -webkit-backdrop-filter: blur(var(--glass-blur)); + backdrop-filter: blur(var(--glass-blur)); + border-bottom: 1px solid var(--glass-border-dark); +} + +#modemenu a { + color: var(--text-color-medium); + text-decoration: none; + font-weight: 600; + font-size: 12.5px; + padding: 5px 10px; + border-radius: 999px; + transition: background .15s ease, color .15s ease; +} + +#modemenu a:hover { + background: var(--glass-bg-hover); + color: var(--text-color-high); +} + +#modemenu .active a { + color: var(--on-primary-color); + background: var(--accent-grad); + box-shadow: 0 3px 10px var(--accent-glow); +} + +#modemenu > div:not(.active) ~ div { + display: none; +} + +/* ========================================================================== + 8. Tab menu + ========================================================================== */ + +/* 通用 tab 按钮组(页面级 #tabmenu 与 cbi 表单内 tab 共用样式)。 + 胶囊形状保留;未选中项透明交给追踪滑块,选中项用菜单区 + selected 同款 accent 渐变高亮。 */ +:is(ul.cbi-tabmenu, ul.tabs) { + position: relative; + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 0 0 14px; + padding: 0; +} + +:is(ul.cbi-tabmenu, ul.tabs) > li { + display: inline-block; + position: relative; + z-index: 1; + margin: 0; +} + +/* 覆盖 LuCI bootstrap 的 tab 容器主题色(bootstrap: color:var(--primary-color-high)), + 统一改为文字色,明暗模式自动切换 */ +:is(ul.cbi-tabmenu, ul.tabs) > li { + color: inherit; +} + +:is(ul.cbi-tabmenu, ul.tabs) > li > a { + display: inline-block; + padding: 8px 18px; + border-radius: 999px; + font-weight: 600; + font-size: 12.5px; + color: var(--text-color-medium); + background: transparent; + border: 1px solid transparent; + text-decoration: none; + transition: color .16s ease, box-shadow .16s ease; +} + +:is(ul.cbi-tabmenu, ul.tabs) > li > a:hover { + color: var(--text-color-high); +} + +/* 选中项与左侧菜单 selected 同款玻璃质感高亮 */ +:is(ul.cbi-tabmenu, ul.tabs) > li.cbi-tab > a { + background: var(--accent-glass); + border-color: color-mix(in srgb, var(--primary-color-high) 55%, transparent); + color: var(--text-color-highest); + box-shadow: 0 4px 16px color-mix(in srgb, var(--primary-color-high) 26%, transparent), + inset 0 1px 0 rgba(255, 255, 255, 0.9); + -webkit-backdrop-filter: blur(10px) saturate(var(--glass-sat)); + backdrop-filter: blur(10px) saturate(var(--glass-sat)); +} + +/* iStoreOS tabs 选中项:Vue scoped 样式(li.active a { color:... })特异性高, + !important 强制主题玻璃样式(与 tabmenu-item 同策略) */ +:is(ul.cbi-tabmenu, ul.tabs) > li.cbi-tab > a { + background: var(--accent-glass) !important; + border-color: color-mix(in srgb, var(--primary-color-high) 55%, transparent) !important; + color: var(--text-color-highest) !important; +} + +/* iStoreOS ul.tabs 的 Vue scoped 滑动条(div.auto)在 flex 容器内占位, + 隐藏它避免干扰 tab 布局(主题自带 .liquid-tab-indicator 追踪滑块) */ +ul.tabs > div.auto { + display: none !important; +} + +:is(:root[data-darkmode="true"] ul.cbi-tabmenu, :root[data-darkmode="true"] ul.tabs) > li.cbi-tab > a { + background: var(--accent-glass); + border-color: color-mix(in srgb, var(--primary-color-high) 48%, transparent); + color: var(--text-color-highest); + box-shadow: 0 4px 18px color-mix(in srgb, var(--primary-color-high) 38%, transparent), + inset 0 1px 0 rgba(255, 255, 255, 0.14); +} + +:is(ul.cbi-tabmenu, ul.tabs) > li.cbi-tab-disabled > a { + opacity: .75; +} + +/* 第三方应用/页面导航 tab(li[class*="tabmenu-item"],如 homeproxy、 + 系统→管理页):显式强制与主题 cbi-tab 完全一致的样式,防任何 + 应用自带样式干扰导致观感不一致 */ +:is(ul.cbi-tabmenu, ul.tabs) > li[class*="tabmenu-item"] > a { + padding: 8px 18px; + border-radius: 999px; + font-weight: 600; + font-size: 12.5px; + color: var(--text-color-medium); + background: transparent; + border: 1px solid transparent; + text-decoration: none; + transition: color .16s ease, box-shadow .16s ease; +} + +:is(ul.cbi-tabmenu, ul.tabs) > li[class*="tabmenu-item"] > a:hover { + color: var(--text-color-high); +} + +:is(ul.cbi-tabmenu, ul.tabs) > li[class*="tabmenu-item"].cbi-tab > a { + background: var(--accent-glass) !important; + border-color: color-mix(in srgb, var(--primary-color-high) 55%, transparent) !important; + color: var(--primary-color-low) !important; + box-shadow: 0 4px 16px color-mix(in srgb, var(--primary-color-high) 26%, transparent), + inset 0 1px 0 rgba(255, 255, 255, 0.9) !important; + -webkit-backdrop-filter: blur(10px) saturate(var(--glass-sat)) !important; + backdrop-filter: blur(10px) saturate(var(--glass-sat)) !important; +} + +:is(:root[data-darkmode="true"] ul.cbi-tabmenu, :root[data-darkmode="true"] ul.tabs) > li[class*="tabmenu-item"].cbi-tab > a { + background: var(--accent-glass) !important; + border-color: color-mix(in srgb, var(--primary-color-high) 48%, transparent) !important; + color: #dcecff !important; + box-shadow: 0 4px 18px color-mix(in srgb, var(--primary-color-high) 38%, transparent), + inset 0 1px 0 rgba(255, 255, 255, 0.14) !important; +} + +/* tab hover 追踪滑块(main.js 驱动,跟随鼠标/选中项滑动) */ +.liquid-tab-indicator { + position: absolute; + left: 0; + top: 0; + bottom: 0; + border-radius: 999px; + background: var(--indicator-bg); + border: 1px solid var(--indicator-border); + box-shadow: var(--indicator-shadow); + opacity: 0; + pointer-events: none; + z-index: 0; + transition: transform .26s cubic-bezier(.22, .8, .3, 1), + width .26s cubic-bezier(.22, .8, .3, 1), + opacity .18s ease; +} + +/* cbi tab pane 显示控制(ui.tabs 通过 data-tab-title / data-tab-active 控制) + 注意:tabmenu 的 li 也带 data-tab 属性,不能按 [data-tab] 全局隐藏, + 必须用 [data-tab-title](仅 pane 内容容器带)折叠内容区——与官方 + bootstrap 的机制一致(height/opacity 折叠而非 display:none)。 */ +[data-tab-title] { + height: 0; + opacity: 0; + overflow: hidden; +} + +[data-tab-active="true"] { + height: auto; + opacity: 1; + overflow: visible; + animation: liquid-fade-in .22s ease; +} + +/* 卡片宽度:逐层按父可用宽 100% 套用;内容超宽时卡片内横向滚动, + 不再溢出外层卡片 —— 仅限 modal 内嵌套(接口编辑等); + 概况页等主内容卡片自适应内部模块高度/宽度,不设滚动条 */ +#modal_overlay .cbi-map, +#modal_overlay .cbi-section { + width: 100%; + overflow-x: auto; +} + +/* 软件包页(package-manager):filter / 下载 / 操作按钮组都是 + div.controls > div > span.control-group > input + button。luci 的 + .controls > * > * { flex-grow:1 } 会把组内 input 和按钮一起拉伸; + 主题全局 input{width:100%} 又让输入框撑满 span,同组按钮被挤出、 + 与相邻控件重叠。控制组改 inline-flex:输入框占剩余空间、按钮按内容宽 */ +.controls .control-group { + display: inline-flex; + align-items: center; + gap: 4px; + max-width: 100%; +} + +.controls .control-group input { + width: auto !important; + min-width: 100px; + flex: 1 1 auto !important; +} + +.controls .control-group .btn { + flex: 0 0 auto !important; + margin: 0; +} + +/* 打开下拉时该层解除裁剪(滚动容器会裁掉弹出的选项列表), + 并提升整条祖先链的层级:backdrop-filter 会让每层都形成层叠 + 上下文,仅提升一层不够,需让包含它的所有容器都高于后续兄弟 */ +.cbi-map:has(.cbi-dropdown[open]), +.cbi-section:has(.cbi-dropdown[open]), +.cbi-section-node:has(.cbi-dropdown[open]), +.cbi-section-table:has(.cbi-dropdown[open]), +#maincontent:has(.cbi-dropdown[open]) { + overflow-x: visible !important; + overflow-y: visible !important; + position: relative; + z-index: 1000; +} + +#tabmenu { + padding: 16px 0 14px; +} + +#tabmenu ul.cbi-tabmenu { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 0; + padding: 0; +} + +#tabmenu ul.cbi-tabmenu > li { + display: inline-block; + margin: 0; +} + +#tabmenu ul.cbi-tabmenu > li > a { + display: inline-block; + padding: 8px 18px; + border-radius: 999px; + font-weight: 600; + font-size: 12.5px; + color: var(--text-color-medium); + background: var(--glass-bg); + border: 1px solid var(--glass-border); + text-decoration: none; + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + box-shadow: var(--glass-shadow-soft); + transition: background .16s ease, color .16s ease, + border-color .16s ease, box-shadow .16s ease; +} + +#tabmenu ul.cbi-tabmenu > li > a:hover { + background: var(--glass-bg-hover); + color: var(--text-color-high); +} + +#tabmenu ul.cbi-tabmenu > .cbi-tab > a { + color: var(--on-primary-color); + background: var(--accent-grad); + border-color: transparent; + box-shadow: 0 6px 18px var(--accent-glow), + inset 0 1px 0 rgba(255, 255, 255, 0.35); +} + +/* ========================================================================== + 9. Footer + ========================================================================== */ + +p.luci { + padding: 20px 22px 30px; + margin: 0; + text-align: center; + font-size: 11.5px; + color: var(--text-color-low); +} + +/* 内容区底部 spacer:滚动到底时内容完整显示在 foot 之上 */ +.luci-foot-spacer { + height: 80px; +} + +/* 登录后 foot 横幅:从菜单栏右外侧到窗口右边缘,顶栏式玻璃横幅 */ +body:not(.liquid-login) p.luci { + position: fixed; + bottom: 0; + left: 236px; + right: 0; + margin: 0; + padding: 9px 22px; + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 8px 14px; + font-size: 11.5px; + color: var(--text-color-low); + background: var(--glass-bg-strong); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + border-top: 1px solid var(--glass-border-dark); + box-shadow: 0 -1px 0 var(--glass-inset), 0 -6px 24px rgba(0, 0, 0, 0.06); + z-index: 700; +} + +body:not(.liquid-login) p.luci a { + color: var(--text-color-medium); + text-decoration: none; + border-bottom: 1px solid transparent; + transition: color .15s ease, border-bottom-color .15s ease; +} + +body:not(.liquid-login) p.luci a:hover { + color: var(--primary-color-high); + border-bottom-color: var(--primary-color-high); +} + +.luci-sep { + opacity: .4; +} + +/* ── 页脚版本号 → 检查更新卡片(对齐 pushbot 的四态 frosted toast)── + overlay 直接挂在 fixed 的页脚 p.luci 内:absolute 锚定页脚盒子, + 水平居中自然是"页脚宽度内"(left:236px → 右缘),不是全屏居中。 + 非更新态 8s 自动消失(比 pushbot 多 4s)+ 点卡片跳作者仓库; + 更新态不自动消失,按钮操作。 */ +.liquid-ver-overlay { + position: absolute; + left: 0; + right: 0; + /* 紧贴页脚上沿向上展开:卡片浮在页脚上方滑出,不遮盖页脚横幅 */ + bottom: 100%; + height: 160px; + z-index: 30; + display: flex; + align-items: flex-end; + justify-content: center; + padding-bottom: 8px; + /* 不要灰黑蒙版/背景模糊:只保留浮出的卡片本身,下方页脚区内容 + 原样可见(用户反馈);卡片自带渐变底 + 投影,不靠蒙版衬托 */ + background: none; + -webkit-backdrop-filter: none; + backdrop-filter: none; + opacity: 0; + transition: opacity .5s ease; + /* 蒙版去掉后 overlay 自身透明,不能再拦在内容上方吃掉点击: + 整层穿透,只有卡片(.liquid-ver-toast)恢复可点 */ + pointer-events: none; +} +.liquid-ver-overlay.show { opacity: 1; } +/* 流程内错误提示(下载失败等):盖在"检测到更新"卡片上、卡片内上下左右 + 居中模糊弹出(对齐 pushbot pb_ota_showError 设计);遮罩随卡片圆角。 + 此态仍需拦截下层卡片的点击,避免错误提示期间误触"立即更新" */ +.liquid-ver-overlay.card-centered { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + height: auto; + z-index: 2; + align-items: center; + justify-content: center; + padding-bottom: 0; + border-radius: inherit; + pointer-events: auto; +} +.liquid-ver-toast { + min-width: 200px; + max-width: min(460px, calc(100vw - 24px)); + padding: 14px 22px; + border-radius: 12px; + position: relative; + text-align: center; + color: #ffffff; + box-shadow: 0 14px 34px rgba(15, 23, 42, 0.35); + transform: scale(.92) translateY(14px); + transition: transform .5s ease; + /* 父层 overlay 已 pointer-events:none,卡片需自行恢复可点 + (按钮操作、点卡片跳作者仓库) */ + pointer-events: auto; +} +.liquid-ver-overlay.show .liquid-ver-toast { transform: scale(1) translateY(0); } +/* 状态色:内测=紫 / 最新=绿 / 有更新=橙 / 失败=红(与 pushbot 一致) */ +.liquid-ver-toast.is-ahead { + background: linear-gradient(135deg, #8b5cf6, #7c3aed); + border: 1px solid rgba(167, 139, 250, 0.45); +} +.liquid-ver-toast.is-latest { + background: linear-gradient(135deg, #10b981, #059669); + border: 1px solid rgba(52, 211, 153, 0.45); +} +.liquid-ver-toast.is-update { + background: linear-gradient(135deg, #f59e0b, #ea580c); + border: 1px solid rgba(251, 146, 60, 0.5); +} +.liquid-ver-toast.is-err { + background: linear-gradient(135deg, #ef4444, #dc2626); + border: 1px solid rgba(248, 113, 113, 0.45); +} +.liquid-ver-toast-msg { + font-size: 14px; + font-weight: 700; + line-height: 1.5; +} +.liquid-ver-toast-sub { + margin-top: 7px; + font-size: 12px; + font-weight: 500; + opacity: .9; + font-family: Menlo, Consolas, monospace; +} +/* 非更新态卡片可点击(整卡超链接作者仓库) */ +.liquid-ver-toast.is-clickable { cursor: pointer; } +/* 页脚版本号:检查中闪烁(同 pushbot badge) */ +.liquid-version-link.is-checking { + opacity: .6; + animation: liquid-blink .8s ease-in-out infinite; +} +@keyframes liquid-blink { + 0%, 100% { opacity: 1; } + 50% { opacity: .4; } +} + +/* ── OTA 操作按钮行(与 pushbot 同款 frosted 白按钮)── */ +.liquid-ota-btn-row { + display: flex; + flex-wrap: nowrap; + gap: 4px; + justify-content: center; + margin-top: 12px; +} +.liquid-ota-btn { + padding: 5px 10px; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, .35); + background: rgba(255, 255, 255, .18); + color: #fff; + font-size: 11px; + font-weight: 600; + cursor: pointer; + transition: background .2s, transform .15s; + display: inline-flex; + align-items: center; + gap: 3px; + line-height: 1.2; + white-space: nowrap; + flex-shrink: 1; + min-width: 0; +} +.liquid-ota-btn:hover { background: rgba(255, 255, 255, .3); transform: translateY(-1px); } +.liquid-ota-btn:active { transform: scale(.96); } +.liquid-ota-btn:disabled { opacity: .55; cursor: not-allowed; transform: none; } +.liquid-ota-btn.liquid-ota-ready { background: rgba(16, 185, 129, .45); border-color: rgba(52, 211, 153, .6); } +/* 圆环进度(One-Click/Pull 下载进度) */ +.liquid-ota-ring { display: inline-block; width: 18px; height: 18px; vertical-align: middle; } +.liquid-ota-ring svg { width: 100%; height: 100%; transform: rotate(-90deg); } +.liquid-ota-ring-bg { fill: none; stroke: rgba(255, 255, 255, .2); stroke-width: 2.8; } +.liquid-ota-ring-fg { + fill: none; + stroke: #fff; + stroke-width: 2.8; + stroke-linecap: round; + transition: stroke-dasharray .35s ease; +} + +/* ========================================================================== + 10. CBI form components + ========================================================================== */ + +.cbi-map { + margin: 0 0 18px; + padding: 18px 22px 22px; + border-radius: var(--glass-radius-lg); + background: var(--glass-bg); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + border: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-inset); +} + +.cbi-map-descr { + color: var(--text-color-medium); + font-size: 13px; + padding: 0 0 12px; +} + +.cbi-section { + margin: 0 0 16px; + padding: 16px 20px 18px; + border-radius: var(--glass-radius); + background: var(--glass-bg); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + border: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow-soft), inset 0 1px 0 var(--glass-inset); +} + +.cbi-section-node { + padding: 4px 0; +} + +/* 多实例 section(如 Dropbear 实例):每个实例独立子卡片。 + 由 main.js 的 styleMultiSections 检测"同一 cbi-section 内有多个 + 平级 cbi-section-node"时加 liquid-multi-node 类(CSS :has 兄弟 + 选择器在部分渲染/嵌套结构下不可靠) */ +.cbi-section-node.liquid-multi-node { + margin: 0 0 12px; + background: color-mix(in srgb, var(--glass-bg) 55%, transparent); + border: 1px solid var(--glass-border); + border-radius: var(--glass-radius); + padding: 12px 16px 10px; + box-shadow: inset 0 1px 0 var(--glass-inset); +} + +.cbi-section-node.liquid-multi-node:last-child { + margin-bottom: 4px; +} + +.cbi-section-node > .cbi-section { + margin-top: 8px; +} + +.cbi-title { + padding: 2px 0 10px; +} + +.cbi-title h3 { + display: flex; + align-items: center; + gap: 10px; + font-size: 15px; + color: var(--text-color-highest); + margin: 0; +} + +.cbi-title h3::before { + content: ""; + width: 4px; + height: 16px; + border-radius: 4px; + background: var(--accent-grad); +} + +/* value rows */ +.cbi-value { + display: flex; + align-items: flex-start; + gap: 16px; + padding: 10px 0; + border-top: 1px solid var(--border-color-low); +} + +.cbi-value:first-of-type { + border-top: 0; +} + +.cbi-value.cbi-value-last { + border-bottom: 0; +} + +.cbi-value-title { + flex: 0 0 260px; + max-width: 46%; + padding-top: 7px; + font-weight: 600; + font-size: 13px; + color: var(--text-color-high); + text-align: right; +} + +.cbi-value-field { + flex: 1; + min-width: 0; +} + +/* 输入框下方的按钮组(如"与浏览器同步/与 NTP 服务器同步"): + span 默认 inline,margin-top 不生效——改 block 并与上方输入框留缝 */ +.cbi-value-field .control-group { + display: block; + margin-top: 8px; +} + +.cbi-value-description { + font-size: 11.5px; + color: var(--text-color-low); + padding-top: 3px; +} + +.cbi-value-field .cbi-value-description { + padding: 3px 0 0; +} + +/* text / number / password / select / textarea */ +input[type="text"], +input[type="password"], +input[type="number"], +input[type="email"], +input[type="tel"], +input[type="url"], +input[type="search"], +input[type="date"], +input[type="time"], +input[type="datetime-local"], +input[type="month"], +input[type="week"], +select, +textarea, +.cbi-input-text, +.cbi-input-password, +.cbi-input-number { + width: 100%; + max-width: 100%; + box-sizing: border-box; + padding: 8px 12px; + font-family: inherit; + font-size: 13px; + color: var(--text-color-high); + background: var(--field-bg); + border: 1px solid var(--border-color-medium); + border-radius: 10px; + -webkit-backdrop-filter: blur(6px); + backdrop-filter: blur(6px); + transition: border-color .15s ease, box-shadow .15s ease, background .15s ease; +} + +input:focus, +select:focus, +textarea:focus { + outline: none; + border-color: var(--primary-color-high); + background: var(--glass-bg-hover); + box-shadow: 0 0 0 3px var(--accent-glow); +} + +input[disabled], +select[disabled], +textarea[disabled] { + opacity: var(--disabled-opacity); + cursor: not-allowed; +} + +/* 原生裸 select(.cbi-input-select 等)现代化:去浏览器原生外观, + 主题圆角/边框/聚焦。此规则作为「不支持 base-select」内核的回退: + Chrome 135+/Edge 135+/Safari 27 会被下方更高特异性的 base-select + 规则覆盖(从而启用 ::picker(select) 自绘下拉);老内核/Firefox 则 + 保留 appearance:none(展开菜单退回系统原生,但折叠态仍是主题样式)。 + 不自绘箭头(避免在 pushbot 等第三方页面出现重复箭头)。 */ +select:not([multiple]) { + appearance: none; + -webkit-appearance: none; + padding: 8px 12px; + background-color: var(--field-bg); + border: 1px solid var(--border-color-medium); + border-radius: 12px; + cursor: pointer; + transition: border-color .15s ease, box-shadow .15s ease; +} + +select:not([multiple]):hover { + border-color: var(--primary-color-high); +} + +/* .cbi-select(分割按钮风格)内部 select 由外层 ::after 提供箭头 */ + +/* 原生 select 弹出列表的选项项美化(浏览器原生面板本身无法定制, + 但 option 的背景/文字色/字体在 Chrome/Edge/Firefox 上可生效) */ +select option { + background: var(--field-bg, #fff); + color: var(--text-color-high); + font-size: 13px; + padding: 4px 8px; +} + +select option:checked { + background: var(--primary-color-high); + color: var(--on-primary-color, #fff); +} + + +textarea { + min-height: 72px; + resize: vertical; +} + +/* checkbox / radio */ +input[type="checkbox"], +input[type="radio"] { + appearance: none; + -webkit-appearance: none; + width: 18px; + height: 18px; + margin: 0; + vertical-align: -4px; + flex-shrink: 0; + cursor: pointer; + background: var(--field-bg); + border: 1.5px solid var(--border-color-high); + transition: background .15s ease, border-color .15s ease, box-shadow .15s ease; +} + +input[type="checkbox"] { + border-radius: 6px; +} + +input[type="radio"] { + border-radius: 50%; +} + +input[type="checkbox"]:checked, +input[type="radio"]:checked { + background: var(--accent-grad); + /* 白边 + 外发光:下拉选项高亮背景(accent)盖住勾选框时, + 勾号和边框仍清晰可见 */ + border: 2px solid rgba(255, 255, 255, 0.9); + box-shadow: 0 0 0 2px var(--accent-glow), 0 2px 8px var(--accent-glow); +} + +input[type="checkbox"]:checked::after { + content: ""; + position: absolute; +} + +input[type="checkbox"]:checked { + position: relative; +} + +input[type="checkbox"]:checked::after { + left: 4px; + top: 0; + width: 6px; + height: 10px; + border: solid #ffffff; + border-width: 0 2.5px 2.5px 0; + transform: rotate(45deg); +} + +input[type="radio"]:checked::after { + content: ""; + position: absolute; + left: 50%; + top: 50%; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--on-primary-color); + transform: translate(-50%, -50%); +} + +input[type="radio"] { + position: relative; +} + +/* buttons */ +.btn, +.cbi-button { + display: inline-block; + /* 原 9px 18px + 1px border;border 移除后各补 1px,整体尺寸不变 */ + padding: 10px 19px; + font-family: inherit; + font-size: 12.5px; + font-weight: 600; + line-height: 1.4; + white-space: nowrap; + color: var(--text-color-high); + text-align: center; + text-decoration: none; + /* 去浏览器原生按钮外观:WebKit 等会叠加一层原生矩形描边, + 不跟 border-radius,出现"两层按钮"的错觉 */ + appearance: none; + -webkit-appearance: none; + background: var(--glass-bg); + /* 不用 border:部分引擎里 border 绘制层会与背景分层栅格化, + 不跟 border-radius 的弧度——圆角处缺描边、左右缘露出方剪裁 + 竖缝/下层按钮。改用同色 inset box-shadow 描边:与背景同层绘制, + 永远贴合圆角(各变体的 border-color 规则随之废弃) */ + border: none; + border-radius: 10px; + cursor: pointer; + /* 按钮不用 backdrop-filter:WebKit/老内核里它的滤镜区域是矩形、 + 不跟 border-radius 裁剪,圆角按钮左右缘会露出方形剪裁的 + 深/浅竖缝,四角也不跟弧度。按钮面积小、模糊收益低, + 玻璃感靠半透明底+描边阴影即可(下拉胶囊同理,见 + .cbi-dropdown.cbi-button-apply / -negative 的覆盖) */ + box-shadow: var(--glass-shadow-soft), inset 0 0 0 1px var(--glass-border), + inset 0 1px 0 var(--glass-inset); + transition: transform .12s ease, box-shadow .15s ease, + background .15s ease, color .15s ease, opacity .15s ease; +} + +.btn:hover, +.cbi-button:hover { + background: var(--glass-bg-hover); + text-decoration: none; +} + +/* 彩色(accent/负面)按钮 hover 保持自身色系并微亮,不发白 */ +.cbi-button-action:hover, +.cbi-button-apply:hover, +.cbi-button-reload:hover, +.cbi-button-edit:hover, +.cbi-button-positive:hover, +.cbi-button-save:hover { + background: var(--accent-grad); + filter: brightness(1.1); +} + +.cbi-button-negative:hover, +.cbi-button-reset:hover, +.cbi-button-remove:hover { + background: linear-gradient(135deg, #f06767, var(--error-color-high)); + filter: brightness(1.08); +} + +.btn:active, +.cbi-button:active { + transform: translateY(1px); +} + +.btn:disabled, +.cbi-button:disabled { + opacity: var(--disabled-opacity); + cursor: not-allowed; +} + +.cbi-button-action, +.cbi-button-apply, +.cbi-button-reload, +.cbi-button-edit, +.btn.primary, +.cbi-button-positive, +.cbi-button-save { + color: var(--on-primary-color); + background: var(--accent-grad); + box-shadow: 0 6px 16px var(--accent-glow), inset 0 1px 0 rgba(255, 255, 255, 0.35); +} + +.cbi-button-negative, +.cbi-button-reset, +.cbi-button-remove, +.cbi-section-remove .cbi-button { + color: var(--on-error-color); + background: linear-gradient(135deg, #f06767, var(--error-color-high)); + box-shadow: 0 6px 16px rgba(var(--error-color-high-rgb), 0.35); +} + +/* 多实例 section(Dropbear 等)的删除按钮:在标题行右侧, + 从卡片右上角向内部收一点,不顶着卡片边缘(float:right 默认贴边)。 + liquid-multi-sec 由 main.js 检测多实例时加到外层 section */ +.cbi-section.liquid-multi-sec > .cbi-section-remove { + margin: 14px 14px 0 0; +} + +.cbi-button-neutral, +.cbi-button-download, +.cbi-button-find, +.cbi-button-link, +.cbi-button-up, +.cbi-button-down, +.cbi-button-pause { + color: var(--text-color-high); + background: var(--glass-bg); + /* 描边由基础规则的 inset 环形阴影提供(原 border-color 已废弃) */ +} + +/* page actions */ +.cbi-page-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + justify-content: flex-end; + padding: 14px 4px 4px; +} + +.cbi-page-actions::after { + content: ""; + clear: both; +} + +/* 原生 select 美化(Combobox 替换已禁用,所有 select 保留原生 DOM)。 + 折叠态 + 展开菜单均通过 CSS 美化(select::picker(select),Chrome 135+/ + Edge 135+/Safari 27 支持),与 Combobox 视觉统一但无 DOM 替换开销。 + 无线设置页 formvalue 联动链因此完全正常。 */ +select:not([multiple]):not(.cbi-dropdown select):not(.cbi-select select) { + /* appearance: base-select 启用 select::picker(select) 自定义下拉菜单, + 不用 appearance: none(那会禁用 picker)。再用显式属性覆盖默认外观。 + 支持 base-select 的浏览器此声明覆盖上方 appearance:none 回退; + 不支持的浏览器此声明被判无效丢弃,自动回退到 appearance:none。 */ + appearance: base-select; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + border-radius: 10px; + padding: 8px 34px 8px 12px; + min-height: 35px; + min-width: 140px; + font-size: 13px; + color: var(--text-color-high); + cursor: pointer; + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + box-shadow: var(--glass-shadow-soft), inset 0 1px 0 var(--glass-inset); + transition: border-color .15s ease, box-shadow .15s ease; +} +select:not([multiple]):not(.cbi-dropdown select):not(.cbi-select select):hover { + border-color: var(--primary-color-high); +} +select:not([multiple]):not(.cbi-dropdown select):not(.cbi-select select):focus { + outline: none; + box-shadow: 0 0 0 2px var(--accent-glow); +} + +/* select 下拉菜单美化(Chrome 135+/Edge 135+/Safari 27 的 select::picker(select)) */ +select:not([multiple]):not(.cbi-dropdown select):not(.cbi-select select)::picker(select) { + appearance: base-select; + background: var(--glass-bg-strong); + border: 1px solid var(--glass-border); + border-radius: 10px; + box-shadow: var(--glass-shadow); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + padding: 4px; + /* 宽度:自适应最长选项内容 + 微余量;禁止撑满到窗口边缘 */ + width: auto; + max-inline-size: calc(100vw - 20px); + overflow: auto; + /* 覆盖浏览器默认 span-inline-end(会横向撑满到右边缘) */ + position-area: block-end; + /* 覆盖浏览器默认 max-block-size: stretch(纵向撑满) */ + max-block-size: auto; +} +/* picker 内选项的背景/文字色:Chrome 不支持 ::picker(select) option 选择器, + 直接用 option 伪类在 picker 内生效的继承机制覆盖。 */ +select:not([multiple]):not(.cbi-dropdown select):not(.cbi-select select) option { + padding: 8px 12px; + color: var(--text-color-high); + transition: background .12s ease; +} +select:not([multiple]):not(.cbi-dropdown select):not(.cbi-select select) option:checked { + background: var(--picker-option-checked); + color: var(--text-color-highest); +} +select:not([multiple]):not(.cbi-dropdown select):not(.cbi-select select) option:hover { + background: var(--picker-option-hover); +} + +/* ========================================================================== + 11. Tables + ========================================================================== */ + +.table, +.cbi-section-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + margin: 6px 0; + border-radius: 12px; + overflow: hidden; + border: 1px solid var(--border-color-low); +} + +.table th, +.cbi-section-table th, +.table .th, +.cbi-section-table .th { + padding: 9px 12px; + font-size: 11.5px; + font-weight: 700; + text-align: left; + letter-spacing: .3px; + color: var(--text-color-medium); + background: var(--glass-bg); + border-bottom: 1px solid var(--border-color-low); + display: table-cell; + float: none; +} + +.table td, +.cbi-section-table td, +.table .td, +.cbi-section-table .td { + padding: 8px 12px; + font-size: 13px; + vertical-align: middle; + border-bottom: 1px solid var(--border-color-low); + background: transparent; + display: table-cell; + float: none; +} + +/* LuCI 新版用 .tr .td 类模拟表格,需要显式设置 display */ +.tr { + display: table-row; +} + +.td { + display: table-cell; + float: none; +} + +.table tr:last-child td, +.cbi-section-table tr:last-child td { + border-bottom: 0; +} + +/* 双色交叉(斑马纹)与 hover 画在整行上,底色延伸到表格行头尾 */ +.table tr.cbi-rowstyle-2, +.cbi-section-table tr.cbi-rowstyle-2, +.table tr:nth-child(even), +.cbi-section-table tr:nth-child(even) { + background: var(--table-stripe); +} + +/* 接口/设备表格行:强制不透明。luci 的"hover 删除按钮"或渲染逻辑 + 会给行加内联 style="opacity:0.5",让已连接接口的图标/文字呈半透、 + 被误判为未连接;统一压回 1(连接状态由图标本身区分) */ +.cbi-section-table tr { + opacity: 1 !important; +} + +.table tr:hover, +.cbi-section-table tr:hover { + background: var(--glass-bg-hover); +} + +/* 用量表格(内存/存储等):表格转 block、行用 flex 强制同行 + (与移动端同一方案;标题列 20ch 允许内部换行、值列占剩余宽度。 + 16ch 会把最长标签"CPU 使用率(%)"挤成两行(实测 16ch 折行、 + 18ch 起单行),20ch 留出中文字体度量的跨平台余量) */ +.table:has(td[width="33%"]) { + display: block; + overflow-x: auto; +} + +.table:has(td[width="33%"]) tr { + display: flex; + align-items: center; + white-space: nowrap; +} + +.table:has(td[width="33%"]) tr > td { + flex: 0 1 auto; +} + +.table:has(td[width="33%"]) tr > td[width="33%"] { + flex: 0 0 20ch; + max-width: 20ch; + white-space: normal; + overflow-wrap: break-word; + word-break: break-word; +} + +.table:has(td[width="33%"]) tr > td:last-child { + flex: 1 1 100%; + white-space: nowrap; +} + +/* ========================================================================== + 12. Dropdown, dynamic list, progressbar + ========================================================================== */ + +.cbi-dropdown { + display: inline-flex; + align-items: center; + position: relative; + min-width: 140px; + /* 多选空值等"无当前值行"(ul 内无 li[display])时 ul 高度为 0, + 胶囊会塌成扁条:给固定最小高度,与含当前值行的胶囊等高 */ + min-height: 35px; + padding: 8px 34px 8px 12px; + font-size: 13px; + /* 闭合:仿左侧菜单选中项——主题色柔和渐变(左浓右淡)叠加玻璃, + 透明带模糊,不再整块 accent 渐变填充 */ + color: var(--text-color-high); + background: + linear-gradient(100deg, + color-mix(in srgb, var(--primary-color-high) 24%, transparent), + color-mix(in srgb, var(--primary-color-high) 5%, transparent) 62%, + color-mix(in srgb, var(--primary-color-high) 12%, transparent)), + var(--glass-bg); + border: 1px solid color-mix(in srgb, var(--primary-color-high) 42%, transparent); + border-radius: 10px; + box-shadow: 0 4px 16px color-mix(in srgb, var(--primary-color-high) 20%, transparent), + inset 0 1px 0 rgba(255, 255, 255, 0.55); + -webkit-backdrop-filter: blur(10px) saturate(var(--glass-sat)); + backdrop-filter: blur(10px) saturate(var(--glass-sat)); + cursor: pointer; +} + +/* 下拉图标区(ui.js 生成的 span.open):绝对定位到右侧,用竖线 + 与按钮主体分割。点击 ▾ 展开下拉;点击左侧主体执行主操作。 + 这是 ComboButton(保存并应用)等下拉按钮的下拉入口, + 不能用背景箭头(点击背景图不命中 span.open,导致无法下拉)。 */ +.cbi-dropdown > .open { + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 34px; + display: flex; + align-items: center; + justify-content: center; + border-left: 1px solid rgba(255, 255, 255, 0.35); + color: var(--on-primary-color); + cursor: pointer; + z-index: 1; +} + +/* cbi-dropdown:未打开时 ul 作为当前值行(只显示选中项), + 打开时变为玻璃下拉列表 */ +.cbi-dropdown > ul { + display: flex; + width: 100%; + margin: 0 !important; + padding: 0; + list-style: none; + overflow: hidden; +} + +.cbi-dropdown > ul > li { + display: none; +} + +/* 当前值行:与官方 bootstrap 一致,无条件显示 li[display] + (不依赖 :not([open]),防止关闭流程中断时值不可见) */ +.cbi-dropdown > ul > li[display] { + display: flex; + align-items: center; + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 多选下拉:选中的多个选项在胶囊内换行展示(每个一行), + 按钮随之动态拓高,页面上直观看得出已选哪些; + 行高随内容在文档流中自然增高,不溢出所在行/卡片 */ +.cbi-dropdown[multiple] > ul { + flex-wrap: wrap; + gap: 2px 8px; +} + +.cbi-dropdown[multiple] > ul > li[display] { + /* 一个选项一行(竖向),不允许并行显示 */ + flex: 0 0 100%; + white-space: normal; + overflow-wrap: break-word; +} + + +/* 打开时:触发器淡出(背景/边框/箭头/预览行全部隐去), + 视觉上只剩一个弹出的选项列表——避免“框 + 弹出列表”看起来像 + 两个下拉菜单堆叠。ui.js 打开时会把原 ul 克隆为 ul.preview, + 弹出规则只作用于 ul.dropdown。 */ +/* 打开时提升自身层级:backdrop-filter 让胶囊形成层叠上下文, + 内部弹出列表 z-index 9999 出不去,会被后续卡片/层盖住 */ +.cbi-dropdown[open] { + z-index: 1000; + /* 打开时胶囊纯玻璃(preview 显示当前值),取消主题色渐变 */ + background: var(--glass-bg); + border-color: var(--glass-border); + box-shadow: var(--glass-shadow-soft), inset 0 1px 0 var(--glass-inset); +} + +/* 暗黑下闭合/打开:外发光略强、内高光调暗(与左侧菜单选中项一致) */ +:root[data-darkmode="true"] .cbi-dropdown { + box-shadow: 0 4px 18px color-mix(in srgb, var(--primary-color-high) 30%, transparent), + inset 0 1px 0 rgba(255, 255, 255, 0.14); +} + +/* 普通下拉(非"保存并应用"类按钮)无分隔:箭头区不画竖线,窄一点 */ +.cbi-dropdown:not(.cbi-button-apply):not(.cbi-button-negative) > .open { + border-left: none; + width: 26px; +} + +/* 普通下拉文字强制主题文字色:on-primary-color 在亮/暗下分别是 + 白/黑,叠在新渐变玻璃上正好白底白字、黑底黑字看不清 */ +.cbi-dropdown:not(.cbi-button-apply):not(.cbi-button-negative), +.cbi-dropdown:not(.cbi-button-apply):not(.cbi-button-negative) > ul > li, +.cbi-dropdown:not(.cbi-button-apply):not(.cbi-button-negative) > ul > li[display] { + color: var(--text-color-high) !important; +} + +:root[data-darkmode="true"] .cbi-dropdown:not(.cbi-button-apply):not(.cbi-button-negative), +:root[data-darkmode="true"] .cbi-dropdown:not(.cbi-button-apply):not(.cbi-button-negative) > ul > li, +:root[data-darkmode="true"] .cbi-dropdown:not(.cbi-button-apply):not(.cbi-button-negative) > ul > li[display] { + color: #d9dfe9 !important; +} + +.cbi-dropdown[open] > .more, +.cbi-dropdown[open] > div { + display: none; +} + +/* ComboButton(保存并应用 / Apply unchecked):保持 accent 渐变按钮 + 样式(而非普通下拉框),右侧竖线分割下拉图标区 */ +.cbi-dropdown.cbi-button-apply { + background: var(--accent-grad); + /* 与 .cbi-button 同理:废弃 border(透明边框也是独立绘制层, + 引擎分层栅格化时不跟 border-radius,露出两层/方剪裁描边)。 + padding 各边 +1px 补偿边框厚度(基础 8px 34px 8px 12px),尺寸不变 */ + border: none; + padding: 9px 35px 9px 13px; + color: var(--on-primary-color); + box-shadow: 0 6px 16px var(--accent-glow), inset 0 1px 0 rgba(255, 255, 255, 0.35); + /* 与 .cbi-button 同理:不透明渐变底本就用不上模糊,去掉继承来的 + backdrop-filter,避免 WebKit 矩形滤镜区域在圆角缘露缝 */ + -webkit-backdrop-filter: none; + backdrop-filter: none; + /* 保存并应用:宽度只匹配文字,不继承普通下拉框的 min-width */ + min-width: auto; +} + +.cbi-dropdown.cbi-button-apply > .open { + border-left-color: rgba(255, 255, 255, 0.35); + color: var(--on-primary-color); +} + +.cbi-dropdown.cbi-button-negative { + background: linear-gradient(135deg, #f06767, var(--error-color-high)); + /* 与 apply 同理:废弃 border 以免独立绘制层不跟圆角;padding +1px 补偿 */ + border: none; + padding: 9px 35px 9px 13px; + color: var(--on-error-color); + box-shadow: 0 6px 16px rgba(var(--error-color-high-rgb), 0.35); + /* 与 apply 同理:不透明渐变底用不上模糊,去掉以免矩形滤镜缝 */ + -webkit-backdrop-filter: none; + backdrop-filter: none; + min-width: auto; +} + +.cbi-dropdown.cbi-button-negative > .open { + border-left-color: rgba(255, 255, 255, 0.35); + color: var(--on-error-color); +} + +/* ComboButton 闭合时:按钮本身已有渐变底色,内部当前值行 + 不再叠加 accent 底色(打开后的弹出列表里选中项仍保留高亮) */ +.cbi-dropdown:not([open]) > ul > li[selected], +.cbi-dropdown.cbi-button-apply:not([open]) > ul > li[selected], +.cbi-dropdown.cbi-button-negative:not([open]) > ul > li[selected] { + background: transparent; +} + +/* 原生 select(.cbi-select):与 cbi-dropdown 统一的分割按钮风格 */ +.cbi-select { + position: relative; + display: inline-flex; + align-items: center; + min-width: 140px; + /* 与按钮族同理:废弃透明 border(独立绘制层不跟圆角), + padding 各边 +1px 补偿(原 8px 34px 8px 12px),尺寸不变 */ + padding: 9px 35px 9px 13px; + /* 与 cbi-dropdown 统一的渐变按钮风格 */ + background: var(--accent-grad); + border: none; + border-radius: 10px; + box-shadow: 0 6px 16px var(--accent-glow), inset 0 1px 0 rgba(255, 255, 255, 0.35); + cursor: pointer; +} + +.cbi-select select { + -webkit-appearance: none; + appearance: none; + width: 100%; + background: transparent; + border: none; + outline: none; + height: auto; + padding: 0; + color: var(--text-color-high); +} + +.cbi-select select option { + background: var(--field-bg); + color: var(--text-color-high); +} + +.cbi-select::after { + content: "▾"; + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 34px; + display: flex; + align-items: center; + justify-content: center; + border-left: 1px solid rgba(255, 255, 255, 0.35); + color: var(--on-primary-color); + pointer-events: none; +} + +/* 单选时多余的 “···”(多选超限指示)默认隐藏,与官方一致 */ +.cbi-dropdown > .more { + display: none; +} + +.cbi-dropdown[multiple][more] > .more { + display: flex; +} + +/* 下拉打开时临时解除卡片链 backdrop-filter(JS holdCardBlur 加类): + 玻璃卡片的 backdrop-filter 创建 stacking context + containing + block,内部下拉 z-index 出不去会被遮挡;解除后下拉可盖住后续卡片 */ +.liquid-dd-bf-off, +.liquid-dd-bf-off * { + -webkit-backdrop-filter: none !important; + backdrop-filter: none !important; +} + +.cbi-dropdown[open] > ul.dropdown { + display: block; + position: absolute; + left: 0; + right: 0; + top: calc(100% + 4px); + z-index: 9999; + max-height: 260px; + overflow-y: auto; + padding: 5px; + background: rgba(255, 255, 255, 0.92); + -webkit-backdrop-filter: blur(var(--glass-blur)); + backdrop-filter: blur(var(--glass-blur)); + border: 1px solid var(--glass-border); + border-radius: 12px; + box-shadow: var(--glass-shadow); +} + +/* 暗黑模式下下拉选项列表更实一些(alpha 0.74 → 0.97 + 更强模糊), + 避免透出下方内容导致选项文字看不清;仍保留玻璃质感 */ +:root[data-darkmode="true"] .cbi-dropdown[open] > ul.dropdown { + background: rgba(22, 29, 44, 0.97); + -webkit-backdrop-filter: blur(32px) saturate(var(--glass-sat)); + backdrop-filter: blur(32px) saturate(var(--glass-sat)); +} + +/* 替换型下拉(liquid-combo-pilot):不用主题色渐变,纯玻璃光色, + 黑白场景各自适配(亮色浅玻璃 / 暗色深玻璃实底) */ +.cbi-dropdown.liquid-combo-pilot { + min-height: 24px; + padding: 4px 34px 4px 10px; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow-soft), inset 0 1px 0 var(--glass-inset); +} + +:root[data-darkmode="true"] .cbi-dropdown.liquid-combo-pilot { + background: rgba(22, 29, 44, 0.92); + border-color: rgba(255, 255, 255, 0.16); +} + +.cbi-dropdown.liquid-combo-pilot > ul > li[display], +.cbi-dropdown.liquid-combo-pilot[open] > ul.preview > li[display] { + padding-top: 2px; + padding-bottom: 2px; +} + +.cbi-dropdown.liquid-combo-pilot > .open { + border-left: none; + width: 24px; + color: var(--text-color-medium); +} + +.cbi-dropdown.liquid-combo-pilot[open] { + background: var(--glass-bg); + border-color: var(--glass-border); + box-shadow: var(--glass-shadow-soft), inset 0 1px 0 var(--glass-inset); +} + +/* 暗黑下闭合框更实(glass-bg 0.4 太透,透出内容会淹没浅色文字): + 已由全局暗色渐变覆盖,此处删除旧实底覆盖 */ + +/* 试点弹出列表悬浮:中性灰高亮,不用主题色 */ +.cbi-dropdown.liquid-combo-pilot[open] > ul.dropdown > li:hover { + background: rgba(127, 127, 127, 0.18) !important; + color: var(--text-color-highest); +} + +/* 打开下拉瞬间:胶囊内 preview(当前值克隆)在鼠标下方会命中全局 + 主题色 hover——强制中性透明,避免"点击时当前选项突然变主题色" */ +.cbi-dropdown.liquid-combo-pilot[open] > ul.preview > li, +.cbi-dropdown.liquid-combo-pilot[open] > ul.preview > li[selected], +.cbi-dropdown.liquid-combo-pilot[open] > ul.preview > li:hover, +.cbi-dropdown.liquid-combo-pilot[open] > ul.preview > li[display]:hover { + background: transparent !important; + color: var(--text-color-high) !important; +} + +/* 打开时胶囊本体跟随全局主题色渐变玻璃(r43 的强制覆盖已由全局 + [open] 样式取代,此处删除) */ + +/* 试点下拉全部文字强制主题文字色(防内联样式/其他规则把闭合或 + 列表文字染成黑色,暗色下看不清) */ +.cbi-dropdown.liquid-combo-pilot > ul > li, +.cbi-dropdown.liquid-combo-pilot > ul > li[display], +.cbi-dropdown.liquid-combo-pilot > ul.preview > li, +.cbi-dropdown.liquid-combo-pilot[open] > ul.dropdown > li { + color: var(--text-color-high) !important; +} + +:root[data-darkmode="true"] .cbi-dropdown.liquid-combo-pilot > ul > li, +:root[data-darkmode="true"] .cbi-dropdown.liquid-combo-pilot > ul > li[display], +:root[data-darkmode="true"] .cbi-dropdown.liquid-combo-pilot > ul.preview > li, +:root[data-darkmode="true"] .cbi-dropdown.liquid-combo-pilot[open] > ul.dropdown > li { + color: #d9dfe9 !important; +} + +/* 试点(接口页 proto,main.js 替换的 Combobox 加 liquid-combo-pilot): + 弹出面板去掉 backdrop-filter blur(性能主因)改用高不透明度半透明底, + 阴影降级——视觉几乎不变,打开/滚动明显更顺。仅试点范围生效。 */ +.cbi-dropdown.liquid-combo-pilot[open] > ul.dropdown { + background: rgba(255, 255, 255, 0.95); + -webkit-backdrop-filter: none; + backdrop-filter: none; + box-shadow: 0 4px 16px rgba(31, 45, 70, 0.14); +} + +:root[data-darkmode="true"] .cbi-dropdown.liquid-combo-pilot[open] > ul.dropdown { + background: rgba(22, 29, 44, 0.97); + -webkit-backdrop-filter: none; + backdrop-filter: none; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.45); +} + +/* 打开时:preview 克隆作为顶部当前值行(官方机制), + 原 ul 弹出后顶部仍有当前值显示 */ +.cbi-dropdown[open] > ul.preview { + display: flex; + flex: 1; + min-width: 0; + align-items: center; + overflow: hidden; +} + +.cbi-dropdown[open] > ul.preview > li[display] { + display: flex; + align-items: center; + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 打开时胶囊内 preview(当前值克隆)无条件透明:点击瞬间鼠标压在其 + 上会命中全局主题色 hover,出现深色渐变背景——所有下拉统一清除 */ +.cbi-dropdown[open] > ul.preview > li, +.cbi-dropdown[open] > ul.preview > li[selected], +.cbi-dropdown[open] > ul.preview > li[display], +.cbi-dropdown[open] > ul.preview > li:hover { + background: transparent !important; +} + +.cbi-dropdown[open] > ul.dropdown > li { + display: flex; + align-items: center; + padding: 7px 10px; + border-radius: 8px; + font-size: 13px; + color: var(--text-color-medium); + cursor: pointer; + /* 长文本选项(如 wechatpush 推送模式带描述)允许换行显示全 */ + white-space: normal; + overflow-wrap: break-word; +} + +/* 富选项(cbiRichListValue):闭合显示标题,弹出列表内显示描述 */ +.cbi-dropdown > ul > li .hide-close, +.cbi-dropdown[open] > ul.dropdown > li .hide-open { + display: none; +} + +.cbi-dropdown > ul > li .hide-open, +.cbi-dropdown[open] > ul.dropdown > li .hide-close { + display: initial; +} + +.cbi-dropdown > ul > li { + padding: 7px 10px; + border-radius: 8px; + font-size: 13px; + color: var(--text-color-medium); + cursor: pointer; +} + +/* 下拉选项 hover:更实的背景 + 高对比文字。 + 亮色用主题色浅 tint(白色背景上白 hover 看不清),暗黑用实色蓝灰 */ +.cbi-dropdown > ul > li:hover { + background: color-mix(in srgb, var(--primary-color-high) 16%, #ffffff); + color: var(--text-color-highest); +} + +:root[data-darkmode="true"] .cbi-dropdown li:hover { + /* 暗黑 hover:半透明白磨砂高亮。 + 注意不能加 backdrop-filter —— li 已位于带玻璃背景的 ul 内, + 再叠一层 blur 会把深色玻璃模糊加深,反而显得比胶囊更暗; + 半透明白叠在深色上即是"凸起的白玻璃"质感 */ + background: rgba(255, 255, 255, 0.07) !important; + color: #ffffff; +} + +.cbi-dropdown > ul > li[selected] { + color: var(--on-primary-color); + background: var(--accent-grad); +} + +/* 下拉弹出列表里的当前选中项:玻璃磨砂白(半透明白,略强于 hover), + 不取主题/胶囊色 —— 胶囊(含保存并应用的 positive/negative 变色)与 + 选中项不再出现"红胶囊 + 绿选项"式的颜色冲突 */ +.cbi-dropdown[open] > ul.dropdown > li[selected] { + background: rgba(255, 255, 255, 0.12) !important; + color: #ffffff; +} + +.cbi-dropdown > .placeholder, +.cbi-dropdown[open] > .placeholder { + color: var(--text-color-low); +} + +.cbi-dynlist { + margin: 4px 0; + padding: 10px; + border-radius: 12px; + background: var(--field-bg); + border: 1px dashed var(--border-color-medium); +} + +.cbi-dynlist .cbi-dynlist-remove { + margin-left: 8px; +} + +.cbi-progressbar { + position: relative; + width: 100%; + min-width: 60px; + height: 8px; + overflow: hidden; + border-radius: 999px; + background: var(--track-bg); +} + +.cbi-progressbar > div { + position: absolute; + left: 0; + top: 0; + bottom: 0; + border-radius: 999px; + background: var(--accent-grad); + box-shadow: 0 0 10px var(--accent-glow); +} + +/* status-page (overview) memory/storage bars: taller, with used/total + text rendered inside the bar by liquid's main.js */ +.cbi-section .cbi-progressbar { + height: 22px; + display: flex; + align-items: center; +} + +.cbi-section .cbi-progressbar > div { + /* fill: sub-menu glass gradient (blue → white), theme color ~90%, + NO border/ring, NO outer drop shadow (it would bleed onto the + row separator on mobile) — edge implied by inner top highlight */ + background: linear-gradient(135deg, + color-mix(in srgb, var(--primary-color-high) 90%, transparent), + rgba(255, 255, 255, 0.72)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8), + inset 0 -1px 0 rgba(0, 0, 0, 0.06); +} + +:root[data-darkmode="true"] .cbi-section .cbi-progressbar > div { + background: linear-gradient(135deg, + color-mix(in srgb, var(--primary-color-high) 88%, transparent), + rgba(46, 58, 84, 0.45)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +.cbi-section .cbi-progressbar > span.liquid-bar-label { + position: relative; + z-index: 2; + margin: 0 auto; + font-size: 11px; + font-weight: 700; + line-height: 22px; + color: var(--text-color-highest); + text-shadow: 0 1px 2px rgba(255, 255, 255, 0.45); + white-space: nowrap; + pointer-events: none; +} + +/* ========================================================================== + 13. Dashboard cards + ========================================================================== */ + +.Dashboard .dashboard-bg { + border-radius: 20px; + background: var(--glass-bg); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + border: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-inset); +} + +.Dashboard .box-s1 { + min-height: 466px; +} + +.Dashboard .title { + text-align: center; + padding: 18px 0 8px; +} + +.Dashboard .title img { + width: 60px; + height: 60px; + object-fit: contain; + vertical-align: middle; +} + +.Dashboard .title h3 { + margin-top: 8px; + font-size: 15px; + color: var(--text-color-highest); +} + +.Dashboard .svgmonotone { + filter: invert(.42) hue-rotate(180deg) saturate(.6); +} + +:root[data-darkmode="true"] .Dashboard .svgmonotone { + filter: invert(.75) hue-rotate(180deg) saturate(.8); +} + +.Dashboard .section-content { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 20px; + padding: 20px 0 0; +} + +.Dashboard .section-content > div { + width: 100%; + padding: 0 1.5em 1.5em; +} + +.Dashboard .section-content > div:nth-child(2) { + margin-left: 0; +} + +.Dashboard .settings-info { + padding-top: 1em; +} + +.Dashboard .settings-info p, +.Dashboard .settings-info .mt-2 { + margin: 6px 0; + font-size: 12.5px; +} + +.Dashboard .settings-info p span:first-child, +.Dashboard .settings-info .mt-2 span:first-child { + display: inline-block; + min-width: 9em; + color: var(--text-color-low); + text-align: right; + margin-right: 6px; +} + +.Dashboard .settings-info p span:nth-child(2) { + color: var(--text-color-high); + font-weight: 500; + word-break: break-all; +} + +.Dashboard .router-status-self .router-status-info .settings-info { + padding-left: 22px; +} + +.Dashboard .internet-status-self .internet-status-info .settings-info, +.Dashboard .router-status-wifi .wifi-info .settings-info, +.Dashboard .router-status-lan .lan-info .settings-info { + display: flex; + justify-content: space-around; + flex-wrap: wrap; + gap: 8px 20px; +} + +.Dashboard .radio-info { + padding: 4px 0; +} + +.Dashboard .devices-list .devices-info .td { + padding: 4px 8px; +} + +.Dashboard .devices-list .devices-info .tr .td:first-child { + width: 33%; + word-break: break-all; +} + +.Dashboard .fade-in { + animation: liquid-fade-in .35s ease; +} + +/* ========================================================================== + 14. Generic app / module card lists + (covers third party "applications" dashboards: name + description cards) + ========================================================================== */ + +.app-item, +.app-card, +.app-box, +.app-entry { + border-radius: var(--glass-radius); + background: var(--glass-bg); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + border: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow-soft), inset 0 1px 0 var(--glass-inset); + padding: 14px 18px; + margin-bottom: 12px; + transition: transform .15s ease, box-shadow .15s ease, background .15s ease; +} + +.app-item:hover, +.app-card:hover, +.app-box:hover, +.app-entry:hover { + background: var(--glass-bg-hover); + box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-inset); + transform: translateY(-1px); +} + +.app-item .app-name, +.app-card .app-title, +.app-box .app-name, +.app-entry .app-name { + font-weight: 700; + font-size: 14px; + color: var(--text-color-highest); +} + +.app-item .app-description, +.app-card .app-description, +.app-box .app-description, +.app-entry .app-description { + font-size: 12px; + color: var(--text-color-medium); + margin-top: 4px; +} + +/* ========================================================================== + 15. Lock screen (sysauth login) + ========================================================================== */ + +body.liquid-login { + background-color: #0a0d13; + background-image: url(img/MontereyDark.jpg); + background-size: cover; + background-position: center; + background-repeat: no-repeat; + background-attachment: fixed; +} + +:root[data-darkmode="false"] body.liquid-login { + background-image: url(img/MontereyLight.jpg); +} + +/* Bing 每日壁纸(仅锁屏界面):壁纸可用(data-liquid-bing-ok="1",由 + ucode 模板按当日缓存是否就绪输出)时覆盖亮/暗两种模式的默认壁纸。 + 无互联网(ok=0)或按钮关闭时规则不匹配,回退默认 Monterey 壁纸 + (亮色 MontereyLight / 暗色 MontereyDark) */ +body.liquid-login[data-liquid-bing-ok="1"] { + background-image: var(--liquid-bing) !important; + background-size: cover; + background-position: center; + background-attachment: fixed; +} + +body.liquid-login .skiplink { + display: none; +} + +body.liquid-login #view .spinning { + display: none; +} + +body.liquid-login #maincontainer { + height: 100vh; +} + +body.liquid-login #liquid-login { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; + overflow: auto; + display: flex; + align-items: center; + justify-content: center; + background: rgba(8, 11, 16, 0.22); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); +} + +/* 登录页纵向 stack:胶囊与登录卡片同处一个 flex 列,胶囊在卡片正上方 + 随卡片高度联动——输错密码出现错误提示使卡片变高时,胶囊自动上移, + 不再与卡片重叠(此前胶囊 absolute 定位在中心上方固定 240px, + 卡片变高不会带动它)。此布局同时天然兼容移动端。 */ +body.liquid-login #liquid-login .liquid-login-stack { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; + max-width: calc(100vw - 40px); +} + +/* 登录页胶囊容器:static 跟随 stack 流式布局(不再 absolute 偏移) */ +body.liquid-login #liquid-login-capsules { + position: static; + transform: none; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 10px; + flex-wrap: nowrap; +} + +/* 登录页胶囊取消 fixed 定位,改为 static 跟随 flex 布局 */ +body.liquid-login .liquid-mode-switch, +body.liquid-login .liquid-color-switch { + position: relative !important; + margin: 0 !important; + overflow: visible; +} +/* 登录页自定义颜色弹出框向上弹出,不被页面底部遮挡 */ +body.liquid-login .liquid-custom-pop { + top: auto; + bottom: calc(100% + 8px); + transform-origin: bottom center; +} +/* 登录页透明度滑杆:胶囊正下方,宽度与胶囊总宽一致,紧凑 */ +body.liquid-login .liquid-glass-slider-login { + position: relative; + left: 0; + right: 0; + top: 0; + display: flex; + align-items: center; + justify-content: center; + height: 5px; + margin-top: -1px; + pointer-events: none; + z-index: 2; + width: calc(100% - 60px); + margin-left: 30px; + margin-right: 30px; +} +body.liquid-login .liquid-glass-slider { + height: 1px; +} +body.liquid-login .liquid-glass-slider::-webkit-slider-thumb { + width: 12px; + height: 12px; + border-width: 1px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 0 0 1.5px rgba(255, 255, 255, 0.1); +} +body.liquid-login .liquid-glass-slider::-moz-range-thumb { + width: 12px; + height: 12px; + border-width: 1px; +} + +/* 登录页隐藏搜索按钮(无意义) */ +body.liquid-login .liquid-search-btn { + display: none !important; +} + +body.liquid-login #liquid-login .modal.login { + width: 392px; + max-width: calc(100vw - 40px); + margin: 0; + padding: 30px 34px 26px; + text-align: center; + border-radius: 24px; + background: var(--lock-card-bg); + -webkit-backdrop-filter: blur(30px) saturate(1.6); + backdrop-filter: blur(30px) saturate(1.6); + border: 1px solid var(--glass-border); + box-shadow: 0 32px 90px rgba(0, 0, 0, 0.55), + inset 0 1px 0 var(--glass-inset); +} + +/* 登录页表单卡片:排除全局 overflow-x:auto(r43 为嵌套卡片滚动而加), + 否则圆角 + 滚动容器会把左上角"用户名"裁掉一角 */ +body.liquid-login .cbi-map, +body.liquid-login .cbi-section { + overflow: visible !important; +} + +/* 登录页 logo(main.js 注入内联 SVG,跟随主题色 + 玻璃水滴感) */ +body.liquid-login #liquid-login .modal.login .liquid-logo { + display: block; + width: 78px; + height: 78px; + margin: 0 auto 14px; +} +body.liquid-login #liquid-login .modal.login .liquid-logo svg { + width: 100%; + height: 100%; + /* 光晕沿水滴轮廓:多层 drop-shadow 从内到外 */ + filter: drop-shadow(0 0 6px color-mix(in srgb, var(--primary-color-high) 50%, transparent)) + drop-shadow(0 2px 12px color-mix(in srgb, var(--primary-color-high) 40%, transparent)) + drop-shadow(0 0 24px color-mix(in srgb, var(--primary-color-high) 20%, transparent)) + drop-shadow(0 1px 0 rgba(255, 255, 255, 0.9)); + transition: filter .3s ease; +} +body.liquid-login #liquid-login .modal.login .liquid-logo:hover svg { + filter: drop-shadow(0 0 10px color-mix(in srgb, var(--primary-color-high) 60%, transparent)) + drop-shadow(0 4px 20px color-mix(in srgb, var(--primary-color-high) 50%, transparent)) + drop-shadow(0 0 36px color-mix(in srgb, var(--primary-color-high) 25%, transparent)) + drop-shadow(0 2px 0 rgba(255, 255, 255, 1)); +} + +body.liquid-login #liquid-login .modal.login h4 { + font-size: 17px; + font-weight: 700; + margin-bottom: 16px; + color: var(--text-color-highest); +} + +body.liquid-login #liquid-login .modal.login .cbi-value { + flex-direction: column; + align-items: stretch; + gap: 5px; + border-top: 0; + padding: 4px 0 12px; +} + +body.liquid-login #liquid-login .modal.login .cbi-value-title { + flex: none; + max-width: none; + width: 100%; + padding-top: 0; + text-align: left; + font-size: 11.5px; + font-weight: 600; + color: var(--text-color-medium); +} + +body.liquid-login #liquid-login .modal.login input { + padding: 11px 13px; + font-size: 14px; + border-radius: 12px; +} + +body.liquid-login #liquid-login .modal.login .alert-message { + text-align: left; + margin: 0 0 8px; +} + +/* avoid nested glass inside the lock card */ +body.liquid-login #liquid-login .modal.login .cbi-map, +body.liquid-login #liquid-login .modal.login .cbi-section, +body.liquid-login #liquid-login .modal.login .cbi-section-node { + margin: 0; + padding: 0; + background: transparent; + border: 0; + box-shadow: none; + -webkit-backdrop-filter: none; + backdrop-filter: none; +} + +body.liquid-login #liquid-login .modal.login button.important { + width: 100%; + margin-top: 6px; + padding: 12px; + font-size: 14px; + border-radius: 12px; + /* 实色主题底:accent-grad 是半透明渐变,在明亮卡片上 + 左边缘会透出深色(看起来像黑边) */ + background: var(--primary-color-high); + border: 1px solid transparent; + box-shadow: 0 6px 16px var(--accent-glow), inset 0 1px 0 rgba(255, 255, 255, 0.35); +} + +body.liquid-login #liquid-login .modal.login hr { + margin: 2px 0 12px; + border-color: var(--border-color-low); +} + +body.liquid-login p.luci { + position: fixed; + bottom: 4px; + left: 0; + width: 100%; + margin: 0; + padding: 8px 22px 4px; + z-index: 2001; + display: flex; + align-items: center; + justify-content: center; + flex-wrap: nowrap; + gap: 0 10px; + color: rgba(255, 255, 255, 0.6); + text-shadow: 0 1px 8px rgba(0, 0, 0, 0.55); +} + +/* 登录页 foot 分隔符 · 更清晰(登录后的共享 .luci-sep 不动) */ +body.liquid-login .luci-sep { + opacity: .7; +} + +body.liquid-login p.luci a { + color: rgba(255, 255, 255, 0.88); + text-decoration: none; + border-bottom: 1px solid rgba(255, 255, 255, 0.35); + transition: color .15s ease, border-bottom-color .15s ease; +} + +body.liquid-login p.luci a:hover { + color: #fff; + border-bottom-color: rgba(255, 255, 255, 0.9); +} + +/* ========================================================================== + 16. Modal dialogs + ========================================================================== */ + +#modal_overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 2000; + overflow: auto; + background: rgba(8, 12, 18, 0.42); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + opacity: 0; + visibility: hidden; + transition: opacity .18s ease, visibility .18s ease; + /* flex 容器:普通 modal 顶部对齐(margin 5em auto), + 应用提示(.alert-message)用 margin:auto 全屏居中。 + 超高 modal 由本容器 overflow:auto 滚动(不锁定、不脱离文档流) */ + display: flex; + align-items: flex-start; + justify-content: center; +} + +body.modal-overlay-active { + overflow: hidden; + height: 100vh; +} + +body.modal-overlay-active #modal_overlay { + left: 0; + right: 0; + opacity: 1; + visibility: visible; +} + +#modal_overlay > .modal { + /* 弹出窗口宽 = 屏幕可用宽度 80%(嵌套卡片按此逐层套宽) */ + width: 80%; + min-width: 270px; + max-width: 80vw; + margin: 5em auto; + min-height: 32px; + border-radius: 18px; + padding: 20px 24px; + background: var(--glass-bg-strong); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + border: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-inset); +} + +/* ── 应用配置流程提示(正在等待/应用完成/无待应用配置等)── + 背景:LuCI 的 displayStatus(type) 会把 type 字符串(如 'notice + spinning')作为 class 直接加到 modal 元素上,而主题的 .notice / + .spinning 是为按钮/文档流元素设计的(margin/padding/border、 + inline-block/relative/padding-left),作用到 modal 自身会把布局 + 挤偏(spinning 的 position:relative 在 flex 中 margin:auto 垂直 + 居中行为异常,是安卓上"正在等待"偏右下的根因)。 + 修复:modal 上下文完整重置这些污染,恢复纯 static + flex 居中。 */ +#modal_overlay > .modal.alert-message, +#modal_overlay > .modal.alert-message.notice, +#modal_overlay > .modal.alert-message.spinning { + /* 重置 .notice 污染(文档流内联提示样式) */ + margin: auto !important; + padding: 20px 28px !important; + border-radius: 18px !important; + font-size: 13px !important; + background: var(--glass-bg-strong) !important; + border: 1px solid var(--glass-border) !important; + /* 重置 .spinning 污染(按钮的 inline-block/relative/padding-left)。 + 注意 position 不用 !important —— JS 强制居中器(visualViewport) + 需用 inline style 覆盖为 fixed,!important 会压死它 */ + position: static; + display: block !important; + text-align: center !important; + width: min(420px, 90vw) !important; + max-width: 90vw !important; + max-height: 80vh !important; + overflow: auto !important; + z-index: 2100 !important; +} + +#modal_overlay > .modal.alert-message h4:empty { + display: none; +} + +/* spinner 图标作为行内元素与文字同行,参与整体居中 */ +#modal_overlay > .modal.alert-message.spinning::before { + position: static; + display: inline-block; + vertical-align: middle; + margin: 0 8px 0 0; +} + +/* 页面顶部成功通知的独立遮罩(portalTopNotices 提升进来的,如"密码已更改"): + 不复用 ui.js 的 #modal_overlay(里面常驻预创建空 .modal 底座, + 混用会把空壳一起显示成两个框),主题自建居中层,点击/6s 自动消失 */ +#liquid-notice-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 2100; + display: none; + align-items: center; + justify-content: center; + overflow: auto; + background: rgba(8, 12, 18, 0.42); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); +} +#liquid-notice-overlay.show { + display: flex; +} +#liquid-notice-overlay .alert-message { + /* 与 #modal_overlay > .modal.alert-message(如"配置更改已应用。") + 完全一致的标准弹窗设计,避免到处不同弹框造成体验割裂 */ + display: block !important; + width: min(420px, 90vw); + max-width: 90vw; + margin: 0; + padding: 20px 28px; + border-radius: 18px; + text-align: center; + font-size: 13px; + cursor: pointer; + /* bootstrap 会给 .info 强制 on-primary 白字 + 蓝渐变,压回主题玻璃 */ + color: var(--text-color-high); + background: var(--glass-bg-strong); + border: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-inset); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + animation: liquid-pop-in .25s ease; +} +#liquid-notice-overlay .alert-message p { + margin: 0; +} +/* 内置"关闭"按钮居中(按钮的 margin-left:auto 是内联样式,需要 !important 压) */ +#liquid-notice-overlay .alert-message > div:last-child { + justify-content: center; +} +#liquid-notice-overlay .alert-message .btn { + margin: 10px 0 0 !important; +} + +#modal_overlay > .modal > * { + line-height: normal; + margin-bottom: .5em; + max-width: 100%; +} + +#modal_overlay > .modal h4 { + font-size: 15px; + color: var(--text-color-highest); +} + +#modal_overlay > .modal .button-row { + display: flex; + justify-content: flex-end; + gap: 10px; + flex-wrap: wrap; +} + +#modal_overlay > .modal .button-row > button:first-of-type { + margin-right: auto; +} + +/* 按钮/操作元素并排时留空隙(表格操作列、内联按钮组、section 创建区等) */ +.cbi-button + .cbi-button, +.cbi-button + button, +button + .cbi-button, +button + button, +.btn + .btn, +.btn + button, +button + .btn, +.td.cbi-section-actions > * + *, +.cbi-section-create > * + * { + margin-left: 8px; +} + +/* 已是 flex gap 的容器(页面操作区、modal 按钮行)自带间距, + 去掉叠加的 margin,避免间距翻倍 */ +.cbi-page-actions > * + *, +#modal_overlay > .modal .button-row > * + * { + margin-left: 0; +} + +/* ========================================================================== + 17. Labels, notices, alerts + ========================================================================== */ + +.label { + display: inline-block; + padding: 3px 10px; + margin: 0 2px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + letter-spacing: .4px; + text-transform: uppercase; + white-space: nowrap; + color: var(--text-color-medium); + background: var(--glass-bg); + border: 1px solid var(--glass-border); +} + +.label.important, +.label.notice { + color: var(--on-primary-color); + background: var(--accent-grad); + border-color: transparent; +} + +.label.success { + color: var(--on-success-color); + background: linear-gradient(135deg, #3dbf88, var(--success-color-high)); + border-color: transparent; +} + +.label.warning { + color: var(--on-warn-color); + background: linear-gradient(135deg, #f0c04a, var(--warn-color-high)); + border-color: transparent; +} + +.label-danger, +.label.error { + color: var(--on-error-color); + background: linear-gradient(135deg, #f06767, var(--error-color-high)); + border-color: transparent; +} + +.alert-message { + margin: 0 0 16px; + padding: 12px 16px; + border-radius: 12px; + background: var(--glass-bg); + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + border: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow-soft); +} + +.alert-message h4 { + font-size: 14px; + margin-bottom: 3px; +} + +.alert-message p { + margin: .25em 0; +} + +.alert-message.warning { + background: color-mix(in srgb, var(--warn-color-high) 22%, var(--glass-bg)); + border-color: color-mix(in srgb, var(--warn-color-high) 45%, transparent); +} + +.alert-message.error { + background: color-mix(in srgb, var(--error-color-high) 20%, var(--glass-bg)); + border-color: color-mix(in srgb, var(--error-color-high) 45%, transparent); +} + +.alert-message.success { + background: color-mix(in srgb, var(--success-color-high) 20%, var(--glass-bg)); + border-color: color-mix(in srgb, var(--success-color-high) 45%, transparent); +} + +.notice { + margin: 8px 0; + padding: 8px 12px; + border-radius: 10px; + font-size: 12px; + background: var(--field-bg); + border: 1px solid var(--border-color-low); +} + +/* ========================================================================== + 18. Misc LuCI components + ========================================================================== */ + +/* loading spinner */ +.spinning { + display: inline-block; + position: relative; + padding-left: 28px; +} + +.spinning::before { + content: ""; + position: absolute; + left: 0; + top: 50%; + width: 20px; + height: 20px; + margin-top: -10px; + color: var(--text-color-medium); + background: url(svg/spinner.svg) center / contain no-repeat; + animation: liquid-spin .9s linear infinite; +} + +/* interface boxes (luci-mod-network) */ +.ifacebox { + display: inline-block; + position: relative; + min-width: 0; + margin: 0 0 16px; + padding: 14px 18px; + border-radius: var(--glass-radius); + background: var(--glass-bg); + -webkit-backdrop-filter: blur(var(--glass-blur)); + backdrop-filter: blur(var(--glass-blur)); + border: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow-soft), inset 0 1px 0 var(--glass-inset); +} + +/* 未加入任何防火墙区域的接口:luci 渲染的 head 色条内联为浅灰/白色 + (#eeeeee / #ffffff),且不同 luci 版本输出不同: + - '--zone-color-rgb:238, 238, 238'(冒号后有无空格两种) + - 或直接 'background-color: rgb(238, 238, 238)' + 统一覆盖变量与直接色值,按模式改为中性灰: + 亮色浅灰(黑字可读),暗色近黑(白字清晰) */ +.ifacebox-head[style*="--zone-color-rgb:238, 238, 238"], +.ifacebox-head[style*="--zone-color-rgb: 238, 238, 238"], +.ifacebox-head[style*="--zone-color-rgb:255, 255, 255"], +.ifacebox-head[style*="--zone-color-rgb: 255, 255, 255"] { + --zone-color-rgb: 125, 125, 125; + background-color: rgb(125, 125, 125) !important; +} + +:root[data-darkmode="true"] .ifacebox-head[style*="--zone-color-rgb:238, 238, 238"], +:root[data-darkmode="true"] .ifacebox-head[style*="--zone-color-rgb: 238, 238, 238"], +:root[data-darkmode="true"] .ifacebox-head[style*="--zone-color-rgb:255, 255, 255"], +:root[data-darkmode="true"] .ifacebox-head[style*="--zone-color-rgb: 255, 255, 255"] { + /* 暗色下:比黑色浅一点点(近黑),白色接口名清晰可读 */ + --zone-color-rgb: 24, 26, 30; + background-color: rgb(24, 26, 30) !important; +} + +.ifacebox-head { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + font-weight: 700; + font-size: 14px; + white-space: nowrap; + color: var(--text-color-highest); + /* 顶部防火墙分区色条:扩展到卡片边缘(抵消 ifacebox 的 + 14px/18px padding),整行显示分区底色,与下方信息区分割 */ + margin: -14px -18px 14px; + padding: 7px 18px; + border-radius: calc(var(--glass-radius) - 2px) calc(var(--glass-radius) - 2px) 0 0; + border-bottom: 1px solid var(--glass-border); +} + +.ifacebox-body { + white-space: nowrap; + text-align: center; +} + +.ifacebox-body > img, +.ifacebox-body > .cbi-tooltip-container, +.ifacebox-body > span { + display: inline-block; + vertical-align: middle; +} + +/* 接口列表操作列(重启/停止/编辑/删除):不压缩、不换行; + 按钮靠左(贴近接口信息,不往右让开太多) */ +.cbi-section-table td.cbi-section-actions { + width: 1%; + white-space: nowrap; + text-align: left; + vertical-align: middle; +} + +/* GridSection(接口页网格卡片)的操作区同样不换行 */ +.cbi-section-actions { + white-space: nowrap; +} + + +/* 端口状态页:接口卡片宽度自适应内容、整体居中、间距 */ +div[style*="grid-template-columns:repeat(auto-fit, minmax(70px, 1fr))"] { + display: flex !important; + flex-wrap: wrap; + justify-content: center; + align-items: flex-start; + gap: 14px; +} + +div[style*="grid-template-columns:repeat(auto-fit, minmax(70px, 1fr))"] .ifacebox, +.network-status-table .ifacebox { + width: auto !important; + max-width: none !important; + min-width: 70px; + margin: 0 !important; +} + +/* GridSection 行内单元格强制等高:接口卡/详情/操作按钮底边对齐, + 单元格分割线在同一高度(内容高度不同时不再错位) */ +div[style*="grid-template-columns:repeat(auto-fit, minmax(70px, 1fr))"] > * { + align-self: stretch !important; + height: auto !important; +} + +/* 接口页行:接口小块靠左(限定左侧),详情与按钮依次排开 */ +div[style*="grid-template-columns:repeat(auto-fit, minmax(70px, 1fr))"]:has(.cbi-section-actions) { + justify-content: flex-start; +} + +/* 网络-接口页的接口卡片:宽度按内容(接口组 bridge 的多个图标)拓展, + 防止图标溢出卡片;max-width 不限制,让列随内容拉伸 */ +.cbi-section-table td[data-name="_ifacebox"] { + width: fit-content; + /* 卡片左右对称留空:多图标扩宽后右侧不贴相邻列 */ + padding-left: 14px; + padding-right: 14px; +} + +.cbi-section-table .ifacebox { + width: fit-content; + min-width: 0; + max-width: none; +} + +/* 端口状态卡片:接口名/图标/连接状态水平居中 */ +div[style*="grid-template-columns:repeat(auto-fit, minmax(70px, 1fr))"] .ifacebox { + text-align: center; +} + +div[style*="grid-template-columns:repeat(auto-fit, minmax(70px, 1fr))"] .ifacebox-head { + justify-content: center; +} + +/* 网络卡片(30_network):内容左对齐、上游标题居中、接口名行居中 */ +.network-status-table .ifacebox { + text-align: left !important; +} + +.network-status-table .ifacebox-body.left, +.network-status-table .ifacebox-body .nowrap { + text-align: left !important; +} + +.network-status-table .ifacebox-head.center { + justify-content: center; + text-align: center; +} + +.network-status-table .ifacebox > div:last-child { + text-align: center; +} + +/* 网络卡片(30_network)整体居中、子卡片留空隙 */ +.network-status-table { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: flex-start; + gap: 14px; + margin-bottom: 14px; +} + +/* 统计 tooltip:默认隐藏,悬停 📊/统计区域时在下方悬浮显示(DHCPv6/端口统计) */ +.cbi-tooltip { + z-index: 9999 !important; + /* absolute 脱离文档流:不占位(否则设备页图标右侧会留白), + 悬停时像概况页一样悬浮显示 */ + position: absolute; + left: -10000px; + opacity: 0; + visibility: hidden; + /* 磨砂玻璃底(与概况页悬浮框一致,否则透明看不见文字) */ + background: var(--glass-bg-strong); + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + border: 1px solid var(--glass-border); + border-radius: 10px; + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.2); + padding: 8px 12px; + white-space: normal; + line-height: 1.55; + color: var(--text-color-high); + pointer-events: none; +} + +.cbi-tooltip-container { + position: relative; +} + +/* 悬浮内容框内不再重复显示接口图标(行内主图标已表示连接状态); + 只针对 render_iface 的 ifacebadge.large 内重复图标,避免误伤 + 设备页等把主图标放在 tooltip 结构里的场景 */ +.cbi-tooltip.ifacebadge.large > img { + display: none; +} + +/* hover 时提升卡片本身的层叠顺序: + .ifacebox 的 backdrop-filter 会创建 stacking context,把 tooltip 的 + z-index:9999 锁在卡片内部,卡片之间的遮挡由 DOM 顺序决定——后一张 + 卡片会盖住前一张卡片的 tooltip。hover 提升卡片(z-index:1001)即可 + 让 tooltip 随卡片整体盖过相邻卡片(tooltip 的 9999 在卡片内部仍生效)。 */ +.ifacebox:hover { + z-index: 1001; +} + +/* 悬浮内容框(tooltip)hover 时,提升其所在卡片本身的层叠顺序。 + 卡片的 backdrop-filter 会创建 stacking context 把 tooltip 的 + z-index 锁在卡片内部,卡片之间的遮挡由 DOM 顺序决定,因此必须 + 整体提升卡片(而非只调 tooltip 的 z-index)。:has 覆盖 tooltip + 可能挂载的各种卡片容器。 */ +.ifacebox:has(.cbi-tooltip-container:hover), +.zonebadge:has(.cbi-tooltip-container:hover), +.cbi-section-node:has(.cbi-tooltip-container:hover), +.cbi-map:has(.cbi-tooltip-container:hover), +.network-status-table:has(.cbi-tooltip-container:hover) { + z-index: 99999 !important; +} + +.ifacebox .cbi-tooltip-container { + position: relative; +} + +.ifacebox .cbi-tooltip { + position: absolute; + left: -10000px; + top: 100%; + opacity: 0; + display: block; + min-width: 220px; + margin: 4px 0 0; + padding: 8px 10px; + border-radius: 10px; + background: var(--glass-bg-strong); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(12px); + border: 1px solid var(--glass-border); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18); + z-index: 9999; + white-space: normal; + line-height: 1.55; + pointer-events: none; + transition: opacity .15s ease; +} + +/* 悬浮内容框被 portal 到 body 顶层后,原 .ifacebox .cbi-tooltip 的 + 背景规则因元素移出卡片而失效(会变成透明、文字看不清), + 这里补上磨砂玻璃底色 */ +.liquid-tip-ported { + background: var(--glass-bg-strong) !important; + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)) !important; + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)) !important; + border: 1px solid var(--glass-border) !important; + border-radius: 10px !important; + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.2) !important; + padding: 8px 12px !important; + white-space: normal !important; + line-height: 1.55 !important; + color: var(--text-color-high) !important; +} + +/* 接口总览表格:列宽自适应 */ +.cbi-section-table { + table-layout: auto; +} + +.cbi-section-table td, +.cbi-section-table .td { + vertical-align: middle; +} + +/* 网络 tooltip 接口徽章:横杠/圆点/接口名/图标同一行垂直居中 */ +.ifacebadge { + display: inline-flex; + align-items: center; + gap: 5px; + vertical-align: middle; +} + +.ifacebadge .zonebadge { + flex-shrink: 0; + padding: 2px 6px; + line-height: 1; +} + +/* 接口图标全局统一 24px:不同 svg viewBox / 不同容器会导致宽度不一, + 统一强制宽度 24px(!important 防内联样式覆盖),占位紧凑 */ +img[src*="icons/"][src$=".svg"] { + width: 24px !important; + height: auto !important; + vertical-align: middle; +} + +/* interfaces.js 里 ifacebox-body 的设备图内联 16px,覆盖为 24px */ +.ifacebox-body > img { + width: 24px !important; + height: auto !important; +} + +.ifacebadge img { + width: 24px; + height: auto; + vertical-align: middle; +} + +/* luci 渲染会给接口徽章(ifacebadge)的图标/文字加内联 + style="opacity:0.5"(离线样式),即使接口已连接也会残留, + 造成半透明、被误判为未连接;统一压回不透明 */ +.ifacebadge img, +.ifacebadge span { + opacity: 1 !important; +} + +.zonebadge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 2px 8px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + background: var(--glass-bg); + border: 1px solid var(--glass-border); + /* 与上方内容(概况页的 ifacebox-body 等)留出空隙 */ + margin-top: 8px; +} + +/* 区域名与成员接口横向排开;成员接口徽章之间用细竖线分隔, + 结构更清晰(区域名 | 接口1 | 接口2) */ +.zonebadge > strong { + margin-right: 2px; +} + +.zonebadge .ifacebadge + .ifacebadge { + border-left: 1px solid currentColor; + padding-left: 8px; +} + +/* 胶囊内接口图标:全局 24px 偏大,缩到 15px 与紧凑胶囊匹配 */ +.zonebadge img[src$=".svg"] { + width: 15px !important; + height: 15px !important; +} + +/* 防火墙区域标签(zonebadge)文字:LuCI 内置的淡 zone 色背景在暗色 + 下不变,白字会看不清——仅对带 zone 色背景的(内联 --zone-color-rgb) + 在暗色下改用深色字;无 zone 色的 empty 标签走玻璃底+继承浅字 */ +:root[data-darkmode="true"] .zonebadge[style*="--zone-color-rgb"] { + color: #1d2633; +} + +/* zonebadge-empty(如 ACCEPT / ACCEPT all others):无 zone 色、玻璃底, + 亮色下默认玻璃边框太淡看不见——用中灰边框;暗色文字继承浅色清晰 */ +.zonebadge-empty { + border-color: var(--border-color-medium); +} + +/* code / pre blocks */ +pre { + padding: 12px 14px; + border-radius: 12px; + background: var(--field-bg); + border: 1px solid var(--border-color-low); + overflow: auto; +} + +/* horizontal form rows helpers */ +.right { + float: right; +} + +.left { + float: left; +} + +/* ========================================================================== + 19. Animations + ========================================================================== */ + +@keyframes liquid-spin { + to { transform: rotate(360deg); } +} + +@keyframes liquid-fade-in { + from { opacity: 0; transform: translateY(4px); } + to { opacity: 1; transform: none; } +} + +@keyframes liquid-jelly { + 0% { opacity: 0; transform: translateY(-10px) scaleY(.88); } + 55% { opacity: 1; transform: translateY(3px) scaleY(1.03); } + 75% { opacity: 1; transform: translateY(-2px) scaleY(.98); } + 100% { opacity: 1; transform: none; } +} + +@keyframes liquid-slide-in { + from { opacity: 0; transform: translateX(-36px); } + to { opacity: 1; transform: none; } +} + +/* ========================================================================== + 20. Responsive + ========================================================================== */ + +@media screen and (max-width: 854px) { + + /* 移动端:弹出窗口(modal)宽 95%(80% 会把内容挤太小); + 左右内边距收到 2px,让第一层卡片也按 2px 缩进(与嵌套每层一致) */ + #modal_overlay > .modal { + width: 95%; + max-width: 95vw; + padding: 16px 2px 20px; + } + + /* 移动端:多层嵌套卡片(network/dns/主机名记录等会套 2-4 层)压缩 + 各层左右缩进到 6px,避免嵌套层数多了最内层内容被挤出视口; + 嵌套的每层再往内缩 2px(margin),层级越深内容越窄。 + 注意左右 padding 需 ≥ 6px:输入框/下拉聚焦高亮 ring 外扩 3px, + 2px 时高亮会被卡片边缘裁断(微超被切),6px 才留足余量 */ + .cbi-map { + padding: 10px 2px 12px; + } + + .cbi-map .cbi-map, + .cbi-map .cbi-section, + .cbi-section .cbi-map, + .cbi-section .cbi-section { + margin-left: 2px; + margin-right: 2px; + } + + .cbi-section { + padding: 10px 6px 12px; + } + + .cbi-section-node { + /* 左右 4px 余量:滚动容器 overflow-x:auto 会在可视右缘横向裁剪 + 输入框聚焦 ring(3px),留 4px 保证 ring 不被切;嵌套仍紧凑 */ + padding: 2px 4px; + } + + /* 登录页表单容器:上面的 overflow-x:auto 同样会横裁登录输入框的 + 聚焦 ring;登录表单不超宽、无需横向滚动,改 visible 即可 */ + body.liquid-login .cbi-section-node { + overflow-x: visible; + } + + .cbi-section-node > .cbi-section { + margin-top: 2px; + } + + /* 移动端:接口卡片列限宽(fit-content 拉伸会把相邻列挤爆, + 拓宽后的卡片与文字重叠),子接口图标行允许换行避免溢出 */ + .cbi-section-table td[data-name="_ifacebox"] { + width: auto; + max-width: 42%; + padding-left: 4px; + padding-right: 4px; + } + + .cbi-section-table .ifacebox { + width: auto; + max-width: 100%; + } + + .ifacebox-body > span { + white-space: normal; + } + + /* 移动端:接口图标强制统一 24px,防止被列宽挤压变形 */ + .cbi-section-table .ifacebox img { + width: 24px !important; + height: auto !important; + flex-shrink: 0; + } + + /* 移动端:接口图标(主图标+子接口图标)换行居中,多图标整齐排列 */ + .ifacebox-body { + white-space: normal; + } + + .ifacebox-body > .cbi-tooltip-container, + .ifacebox-body > span, + .ifacebox-body > span .cbi-tooltip-container { + display: inline-block; + vertical-align: middle; + margin: 2px; + } + + /* 移动端:接口表格按内容宽度排布(列不压缩,按钮紧贴信息: + 不重叠、也不贴最右留空);超出视口时外层横向滚动 */ + .cbi-section-table { + width: max-content; + min-width: 100%; + } + + /* 移动端所有表格按内容宽度排布(列不压缩,MAC/MTU 等不重叠), + 超出视口时允许横向滚动。 + 注意:横向滚动只放在表格的直接容器(.cbi-section-node 包裹式 / + .cbi-tblsection 直挂式,form.js 的两种渲染路径),不再放在 + #maincontent——#maincontent 一旦带 overflow-x:auto 就成了嵌套滚动 + 容器,安卓 Chrome 对其上的纵向手势路由比 iOS 严格,会拖慢/拦掉 + 文档级滚动从而挡住地址栏收缩;宽表格在自己的容器内横滚即可 */ + .cbi-section-node, + .cbi-tblsection { + overflow-x: auto; + } + + .cbi-section-table { + width: max-content !important; + min-width: 100%; + } + + .cbi-section-table td.cbi-section-actions { + white-space: nowrap; + } + + /* 接口页 GridSection(网格卡片,带操作按钮的行):按内容宽度排布, + 重启/停止/编辑/删除按钮不挤压重叠文字,超出视口横向滚动 */ + div[style*="grid-template-columns:repeat(auto-fit, minmax(70px, 1fr))"]:has(.cbi-section-actions) { + width: max-content; + min-width: 100%; + overflow-x: auto; + } + + /* 接口小框(接口名 + 设备/IP 信息)与其所在行卡片左缘间隔 4px: + 把 td[data-name="_ifacebox"] 的左右 padding 从 14px 收到 4px + (小框往左移;对称留空保持,右侧也不贴相邻列) */ + .cbi-section-table td[data-name="_ifacebox"] { + padding-left: 4px; + padding-right: 4px; + } + + /* 移动端接口图标:宽度固定 24px,高度等比缩放(不强制高度) */ + img[src*="icons/"][src$=".svg"] { + width: 24px !important; + height: auto !important; + } + + /* 移动端接口页表格(含接口图标列的 GridSection):锁定视口宽, + 覆盖上面的 max-content(行不再按内容撑宽、按钮不再溢出到屏幕 + 右侧),所有内容在视口内换行显示 */ + .cbi-section-table:has(td[data-name="_ifacebox"]) { + width: 100% !important; + min-width: 100% !important; + max-width: 100% !important; + } + + /* 移动端接口页:每接口两行 —— 第一行 = 接口图标 + 详细信息, + 第二行 = 重启/停止/编辑/删除四个操作按钮(整行,右对齐) */ + .cbi-section-table-row { + display: flex; + flex-wrap: wrap; + /* 图标列与详细信息列等高对齐;二者合在一起超宽时才允许分行。 + 横向 gap 归零:各 td 本来自带的分割线(border-bottom)就能 + 首尾相连成一条连续线;垂直 gap 保留作换行/与按钮行的间距 */ + align-items: stretch; + gap: 6px 0; + } + + .cbi-section-table-row > td { + flex: 0 1 auto; + min-width: 0; + /* 移动端接口页:隐藏行分割线(线与列对齐难兼顾,先试无分割线) */ + border-bottom: 0 !important; + } + + .cbi-section-table-row > td.cbi-section-actions { + flex: 1 1 100%; + order: 99; + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + gap: 4px; + padding: 2px 0 4px; + /* 四个按钮下方多 8px:让相邻两个接口之间稍微区分 */ + margin-bottom: 8px; + } + + #menubar .navigation { + display: flex; + } + + #maincontainer { + flex-direction: column; + height: calc(100vh - 64px); + } + + #mainmenu { + position: fixed; + left: 0; + top: 64px; + bottom: 0; + width: 264px; + transform: translateX(-102%); + animation: none; + z-index: 800; + overflow-y: auto; + overscroll-behavior: contain; + box-shadow: none; + } + + #mainmenu.active { + transform: translateX(0); + box-shadow: 0 14px 50px rgba(0, 0, 0, 0.35); + } + + #maincontent { + padding: 0 16px 64px; + margin-left: 0; + /* r19 起移动端页面纵向滚动由文档承担(见下方「文档级滚动」块, + 为了让安卓浏览器地址栏随滚动收缩)。此处 flex-shrink:0 保证 + #maincontent 不被压缩成滚动容器;overflow-x:auto 只负责宽表格 + 横向滚动。下拉打开时 :has(.cbi-dropdown[open]) 把 overflow 翻回 + visible 不再影响文档滚动位置(滚动位置在文档上,与此处无关) */ + flex-shrink: 0; + } + + /* ===== 文档级滚动:让安卓 Chrome/Edge 地址栏随滚动收缩(r19)===== + 安卓只在「文档级滚动」时才收缩工具栏/地址栏。此前纵向滚动锁在 + #maincontainer(body{overflow:hidden} + #maincontainer{overflow-y:auto}) + 这个嵌套滚动容器里,浏览器检测不到文档滚动,地址栏固定占高。 + 现改为文档自然滚动:body 放开 overflow、#maincontainer 去掉固定高度 + 与内层滚动、#menubar 靠 sticky 吸顶。仅登录后长页面生效,登录页 + (body.liquid-login)保持原固定布局。宽表格横向滚动仍由 + #maincontent{overflow-x:auto} 承担,不会外溢成文档横向滚动。 */ + html { + overflow: visible; + height: auto; + } + + body:not(.liquid-login) { + overflow: visible; + height: auto; + /* 用 svh(小视口=工具栏全展开的高度)而非 dvh:dvh 会随工具栏 + 收缩动画实时变化,导致 body 高度回流、与收缩手势互相干扰 + (症状:须先回顶才肯收缩)。svh 恒定,背景经 canvas 铺满全屏 */ + min-height: 100vh; + min-height: 100svh; + /* 安卓 Chrome/Edge 会禁用 background-attachment:fixed 页面的 + 地址栏收缩(fixed 背景需整幅重绘,浏览器直接关掉收缩动画)。 + 移动端改 scroll:渐变沿整个文档铺开,视觉无断层 */ + background-attachment: scroll; + } + + body:not(.liquid-login) #maincontainer { + height: auto; + min-height: 0; + overflow: visible; + } + + /* ===== 移动端 chrome 条去模糊(r23)===== + 安卓 Chrome/Edge 的地址栏收缩动画期间要重绘 sticky/fixed 元素背后 + 区域,backdrop-filter 元素需重采样、与收缩动画冲突——症状: + 首次下滑不收缩,往上回一点点再下滑才行;iOS Safari 滤镜层无此问题。 + 吸顶菜单栏/面包屑、fixed 底部 foot、菜单抽屉全部去掉 blur,改等价 + 不透明实色(与 meta theme-color 同色,浏览器工具栏依旧无缝衔接)。 + 内容区卡片的 backdrop-filter 保留(在文档流内,不参与该冲突) */ + #menubar, + #modemenu, + #mainmenu, + body:not(.liquid-login) p.luci { + -webkit-backdrop-filter: none; + backdrop-filter: none; + } + + #menubar, + #modemenu, + body:not(.liquid-login) p.luci { + background: var(--glass-chrome-solid); + } + + #mainmenu { + background: var(--glass-menu-solid); + } + + body:not(.liquid-login) p.luci { + left: 0; + } + + /* 移动端登录页 foot:窄屏允许换行(桌面一行) */ + body.liquid-login p.luci { + flex-wrap: wrap; + gap: 2px 10px; + bottom: 2px; + } + + /* 移动端 foot 横幅只保留「主题版本 + 固件版本」(版本号需可点检查更新), + 其余(kernel/luci/多余分隔符/检查更新卡片容器)仍隐藏。 + 注意 overlay 也是 p.luci 的直接子元素——不排除会被这条规则 + display:none 掉,移动端整个检查更新卡片直接消失 */ + body:not(.liquid-login) p.luci > *:not(.luci-fw):not(.luci-theme-ver):not(.luci-sep-fw):not(.liquid-ver-overlay) { + display: none; + } + + body:not(.liquid-login) p.luci { + /* 移动端 foot 加高 4px(上下 padding 各 +2px),与浏览器底部 + 工具栏衔接更饱满 */ + padding: 8px 22px; + /* 字号调小一号 */ + font-size: 10.5px; + /* 主题版本与固件版本间隔:较桌面 14px 大幅收紧 */ + gap: 8px 4px; + /* 主题版本+固件版本一行放不下时:不换行、不压缩, + 从左侧起排,右侧溢出屏幕的部分直接裁掉不显示 */ + flex-wrap: nowrap; + justify-content: flex-start; + /* 关键:overflow:hidden 会把"向上滑出页脚"的检查更新卡片 + 整个裁掉(移动端点击无弹出的根因)。 + overflow-x: clip 只裁横向溢出,且 clip/visible 配对不会 + 把 overflow-y 降级为 auto —— 横向照裁、纵向放行卡片 */ + overflow-x: clip; + overflow-y: visible; + } + body:not(.liquid-login) p.luci > * { + flex-shrink: 0; + white-space: nowrap; + } + + /* 更新卡片按钮:窄屏一排 6 个太挤 → 3+3 两行网格。 + grid 自动布局同样遵循 flex 的 order(order-modified document order), + 用 order 重排到目标顺序、列数固定 3,行划分确定不依赖宽度; + DOM 顺序不变,桌面端(media 外)仍是原来的一行。 + 第一行:一键更新 / 拉取新包 / 清理包 + 第二行:下载链接 / 作者仓库 / 忽略 */ + .liquid-ota-btn-row { + display: grid; + grid-template-columns: repeat(3, auto); + justify-content: center; + justify-items: center; + gap: 8px; + } + .liquid-ota-btn-row .liquid-ota-btn:nth-child(1) { order: 1; } /* 一键更新 */ + .liquid-ota-btn-row .liquid-ota-btn:nth-child(2) { order: 2; } /* 拉取新包 */ + .liquid-ota-btn-row .liquid-ota-btn:nth-child(4) { order: 3; } /* 清理包 */ + .liquid-ota-btn-row .liquid-ota-btn:nth-child(3) { order: 5; } /* 下载链接 */ + .liquid-ota-btn-row .liquid-ota-btn:nth-child(5) { order: 6; } /* 作者仓库 */ + .liquid-ota-btn-row .liquid-ota-btn:nth-child(6) { order: 7; } /* 忽略 */ + + /* 更新卡片占屏宽 85%,按钮增大(触屏点按更友好) */ + .liquid-ver-toast { + width: 85vw; + } + .liquid-ota-btn { + padding: 8px 12px; + font-size: 12px; + } + + .luci-foot-spacer { + height: 100px; + } + + #menubar .distversion { + display: none; + } + + /* 文档级滚动后 modemenu(面包屑)需 sticky 吸顶在菜单栏下方, + 否则会被当普通内容随文档滚走(内层滚动时期它是 static,因为在 + 滚动容器外、天然不动;改文档滚动后必须显式吸顶) */ + #modemenu { + position: sticky; + top: 56px; + } + + .cbi-value { + flex-direction: column; + align-items: stretch; + gap: 5px; + } + + .cbi-value-title { + flex: none; + max-width: none; + width: 100%; + padding-top: 0; + text-align: left; + } + + .cbi-section-table, + .table { + display: block; + overflow-x: auto; + } + + /* 移动端:tr 转 flex。分隔线画在 tr 底部(而非各 td)—— + 行内 td 高度不同(标题行矮、进度条行高),td 级 border-bottom + 会左右错位;tr 级 border-bottom 只有一条边,横贯整行天然一致。 + (斑马纹/hover 本就画在 tr 上,故左右始终对齐) */ + .cbi-section-table tr, + .table tr { + display: flex !important; + align-items: stretch; + white-space: nowrap; + border-bottom: 1px solid var(--border-color-low); + } + + .cbi-section-table tr > td, + .cbi-section-table tr > th, + .cbi-section-table tr > .td, + .cbi-section-table tr > .th, + .table tr > td, + .table tr > th, + .table tr > .td, + .table tr > .th { + flex: 0 1 auto; + display: flex; + align-items: center; + /* 基础规则用类选择器(.table .td),特异性更高,必须 !important + 才能移除 td 级分隔线(分隔线统一画在 tr 底部) */ + border-bottom: 0 !important; + } + + .cbi-section-table tr:last-child, + .table tr:last-child { + border-bottom: 0; + } + + /* 移动端:进度条行(22px 高)与文字行高度差会让分隔线节奏不均, + 收紧进度条行的上下 padding,让行高与文字行一致,横线间距均匀。 + 同时给该行 td 显式 min-height(22px 进度条 + 4px*2 padding), + 兜底 stretch 未拉高左侧标题 td 的情况 → 左右 border-bottom 对齐 */ + .cbi-section-table tr:has(.cbi-progressbar) > td, + .table tr:has(.cbi-progressbar) > td { + padding-top: 4px; + padding-bottom: 4px; + min-height: 30px; + } + + .cbi-section-table tr > td:last-child, + .table tr > td:last-child { + flex: 1 1 100%; + } + + .cbi-section-table tr > td[width="33%"], + .table tr > td[width="33%"] { + flex: 0 0 20ch; + max-width: 20ch; + white-space: normal; + overflow-wrap: break-word; + word-break: break-word; + } + + .Dashboard .section-content { + flex-direction: column; + } + + .Dashboard .box-s1 { + min-height: 0; + } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: .01ms !important; + transition-duration: .01ms !important; + } +} + + +/* Bootstrap 工具类:插件常用 class="hidden" 隐藏元素(隐藏的保存按钮、 + 提示面板、折叠区块等)。主题只加载自己的 css,必须自行提供, + 否则插件声明 hidden 的元素会全部显示出来。 */ +.hidden { + display: none; +} + +/* ===== 下拉浮层突破卡片裁剪(纯 CSS,不动 JS/DOM) ===== + 1) 含下拉的表格:overflow:hidden(圆角裁剪)→ visible,弹出列表可见; + 2) 含"打开"下拉的卡片提升 z-index:突破 backdrop-filter 形成的 + stacking context,避免被相邻卡片盖住; + 3) 移动端滚动容器(.cbi-section-node / #maincontent):两轴 visible, + 不再裁剪弹出列表。r19 起页面纵向滚动由文档承担(#maincontent 仅 + overflow-x:auto 负责宽表格横滚),此翻转不影响文档滚动位置 */ +/* 只在"打开"时解除裁剪(加载/闭合时保持原样,不影响胶囊尺寸) */ +.cbi-section-table:has(.cbi-dropdown[open]) { + overflow: visible !important; +} + +.cbi-section:has(.cbi-dropdown[open]), +.cbi-map:has(.cbi-dropdown[open]) { + position: relative; + z-index: 1000; +} + +@media screen and (max-width: 854px) { + .cbi-section-node:has(.cbi-dropdown[open]), + #maincontent:has(.cbi-dropdown[open]) { + overflow-x: visible !important; + overflow-y: visible !important; + } +} +/* ========================================================================== + OpenClash 页面适配(透明玻璃卡片 + 内容居中) + status 页上下两块大卡片(.main-card,各自嵌套 .card-row > .sub-card + 多层)全部主题化:大卡片透明融入页面背景,内层 sub-card / 公告卡片 + 用主题玻璃配方(半透明 + 模糊 + 玻璃边框 + 内高光)。明暗由 + openclash 自带变量(html[data-darkmode="true"] .oc)与主题 + --glass-* 变量联动,无需单独处理暗色。 + ========================================================================== */ + +/* 大卡片(两块 .main-card):去掉白底/边框/阴影,透明融入主题背景 */ +body[data-page^="admin-services-openclash"] #cbi-openclash > fieldset .main-card { + background: transparent !important; + border: none !important; + box-shadow: none !important; + padding: 0 10px 10px 10px; +} + +body[data-page^="admin-services-openclash"] #cbi-openclash > fieldset .myip-main-card { + margin: 0; + padding: 10px; + border: none !important; + background: transparent !important; +} + +/* 公告卡片:玻璃 */ +body[data-page^="admin-services-openclash"] .oc .announcement-card { + background: var(--glass-bg) !important; + border: 1px solid var(--glass-border) !important; + border-radius: var(--glass-radius); + box-shadow: var(--glass-shadow-soft), inset 0 1px 0 var(--glass-inset) !important; + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); +} + +/* 内层嵌套卡片(sub-card):主题玻璃配方,替代 openclash 白/灰实底 */ +body[data-page^="admin-services-openclash"] .oc .sub-card { + background: var(--glass-bg) !important; + border: 1px solid var(--glass-border) !important; + border-radius: var(--glass-radius); + box-shadow: var(--glass-shadow-soft), inset 0 1px 0 var(--glass-inset) !important; + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); +} + +/* hover:主题玻璃高亮(主色边框 + tint 背景),去掉 openclash 位移 */ +body[data-page^="admin-services-openclash"] .oc .sub-card:hover { + background: var(--glass-bg-hover) !important; + border-color: color-mix(in srgb, var(--primary-color-high) 55%, var(--glass-border)) !important; + box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-inset) !important; + transform: none; +} + +/* myip 页:IP 地址 / 访问检查 输出块玻璃化 */ +body[data-page^="admin-services-openclash"] .oc .myip-ip-item, +body[data-page^="admin-services-openclash"] .oc .myip-check-item { + background: var(--glass-bg) !important; + border: 1px solid var(--glass-border) !important; + border-radius: var(--glass-radius); + box-shadow: var(--glass-shadow-soft), inset 0 1px 0 var(--glass-inset) !important; + -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); + backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat)); +} + +body[data-page^="admin-services-openclash"] .oc .myip-ip-item:hover, +body[data-page^="admin-services-openclash"] .oc .myip-check-item:hover { + background: var(--glass-bg-hover) !important; + border-color: color-mix(in srgb, var(--primary-color-high) 55%, var(--glass-border)) !important; + box-shadow: var(--glass-shadow), inset 0 1px 0 var(--glass-inset) !important; + transform: none; +} + +/* ---- 内容居中(运行状态 / 控制面板 IP:端口 / 混合代理) ---- */ +body[data-page^="admin-services-openclash"] .oc .card-content { + align-items: center !important; + justify-content: center !important; +} + +body[data-page^="admin-services-openclash"] .oc .card-controls { + align-self: center; +} + +body[data-page^="admin-services-openclash"] .oc .core-main-controls { + justify-content: center !important; +} + +body[data-page^="admin-services-openclash"] .oc .plugin-toggle-container { + margin-left: inherit !important; +} + +body[data-page^="admin-services-openclash"] .oc .core-status-toggle { + flex: 0 !important; + min-width: auto !important; +} + +body[data-page^="admin-services-openclash"] .oc .card-value { + justify-content: center; +} + +/* ========================================================================== + 宽表格优先折叠按钮(liquid-actions-overflow) + ------------------------------------------------------------------------- + 触发:main.js 判定表格“自然宽度”超出所在容器 / 当前屏幕时给 + 加 liquid-actions-overflow 类(判定始终在去类状态下进行,避免加/去类 + 来回震荡;仅作用于含操作列的表格,无按钮的表格维持原样)。 + + 效果:操作列先让步,而不是先压缩文字—— + 1) 放开 form.js stabilizeActionColumnWidth 写死在 td/th 上的内联 + width/min-width(必须 !important 才能压过内联样式); + 2) 一旦进入折叠状态,按钮**每个独占一行**(列宽收到最宽按钮的 + 内容宽),不做“随宽度 2+2+1 逐步折行”的渐进换行——行为确定, + 文字列也能拿回最多宽度。注意 LuCI 用 dom.append 逐个追加按钮, + 按钮之间没有空白文本节点,inline 元素之间不存在换行点,只改 + white-space 不会换行,必须让按钮容器(td 内的 div)转 flex; + 3) 文字列因此拿回可用宽度,不再被挤成窄条多行换行 / 被容器裁切; + 4) 窄屏(≤854px)表格按 max-content 撑宽(=行内所有格子排成一行的 + 总宽),按钮换行也拉不窄——把表宽收回 100%,行内换行才生效, + 同时去掉横向滚动;此时操作列已独占整行、按钮放得下,维持一行 + 横排(放不下时仍可 flex-wrap 折行)。 + 按钮已折叠仍放不下时(文字本身超宽),文字单元格换行兜底。 */ +table.liquid-actions-overflow td.cbi-section-actions, +table.liquid-actions-overflow th.cbi-section-actions { + white-space: normal !important; + /* 不设首选宽度:列宽=最宽按钮的内容宽(百分比会抢文字列的可用宽度) */ + width: auto !important; + min-width: 0 !important; + flex-wrap: wrap; + gap: 4px 6px; +} + +table.liquid-actions-overflow td.cbi-section-actions > div { + display: flex; + /* 每个按钮一行(纵排、靠左,按钮保持自然宽度不被拉伸) */ + flex-direction: column; + align-items: flex-start; + gap: 4px 6px; + width: auto !important; + min-width: 0 !important; + max-width: 100%; +} + +/* 已有 gap,清掉相邻按钮的 margin-left(否则间距叠加翻倍) */ +table.liquid-actions-overflow td.cbi-section-actions > div > * + * { + margin-left: 0 !important; +} + +/* 兜底:按钮已折叠后表格仍放不下(文字本身超宽,如长域名)时, + 让文字单元格换行收缩,而不是被 .cbi-section 的 overflow:hidden + 裁掉右侧——优先级仍是"先折叠按钮,最后才让文字换行" */ +table.liquid-actions-overflow td:not(.cbi-section-actions), +table.liquid-actions-overflow th:not(.cbi-section-actions) { + white-space: normal !important; + overflow-wrap: break-word; + word-break: break-word; +} + +@media screen and (max-width: 854px) { + table.liquid-actions-overflow { + width: 100% !important; + min-width: 100% !important; + } + + /* 窄屏下行内布局已把操作列推到独占的一整行、宽度足够摆下所有按钮, + 维持横排一行;真的放不下时 flex-wrap 照样折行 */ + table.liquid-actions-overflow td.cbi-section-actions > div { + flex-direction: row; + flex-wrap: wrap; + align-items: center; + } +} diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/alias.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/alias.svg new file mode 100644 index 000000000..e7f0fec87 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/alias.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/alias_disabled.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/alias_disabled.svg new file mode 100644 index 000000000..29b2d405b --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/alias_disabled.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/bridge.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/bridge.svg new file mode 100644 index 000000000..e7f0fec87 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/bridge.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/bridge_disabled.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/bridge_disabled.svg new file mode 100644 index 000000000..29b2d405b --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/bridge_disabled.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/encryption.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/encryption.svg new file mode 100644 index 000000000..6117f70df --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/encryption.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/encryption_disabled.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/encryption_disabled.svg new file mode 100644 index 000000000..657f992d7 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/encryption_disabled.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/ethernet.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/ethernet.svg new file mode 100644 index 000000000..f8ba6553a --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/ethernet.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/ethernet_disabled.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/ethernet_disabled.svg new file mode 100644 index 000000000..27a4fb51f --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/ethernet_disabled.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/loading.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/loading.svg new file mode 100644 index 000000000..f6907c1b3 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/loading.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/port_down.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/port_down.svg new file mode 100644 index 000000000..026c2ebb1 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/port_down.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/port_up.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/port_up.svg new file mode 100644 index 000000000..d7471933c --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/port_up.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/signal-000-000.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/signal-000-000.svg new file mode 100644 index 000000000..fd560f7a6 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/signal-000-000.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/signal-000-025.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/signal-000-025.svg new file mode 100644 index 000000000..364b9b3e5 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/signal-000-025.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/signal-025-050.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/signal-025-050.svg new file mode 100644 index 000000000..9f62e3d4e --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/signal-025-050.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/signal-050-075.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/signal-050-075.svg new file mode 100644 index 000000000..4a7be21c5 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/signal-050-075.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/signal-075-100.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/signal-075-100.svg new file mode 100644 index 000000000..778f26f21 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/signal-075-100.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/signal-none.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/signal-none.svg new file mode 100644 index 000000000..db4720644 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/signal-none.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/switch.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/switch.svg new file mode 100644 index 000000000..2e5dcdffd --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/switch.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/switch_disabled.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/switch_disabled.svg new file mode 100644 index 000000000..948babddd --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/switch_disabled.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/tunnel.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/tunnel.svg new file mode 100644 index 000000000..0bbeae21d --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/tunnel.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/tunnel_disabled.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/tunnel_disabled.svg new file mode 100644 index 000000000..c0f0facc2 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/tunnel_disabled.svg @@ -0,0 +1,394 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/vlan.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/vlan.svg new file mode 100644 index 000000000..b8435b788 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/vlan.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/vlan_disabled.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/vlan_disabled.svg new file mode 100644 index 000000000..ffa498876 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/vlan_disabled.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/vrf.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/vrf.svg new file mode 100644 index 000000000..7c9c96be7 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/vrf.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/vrf_disabled.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/vrf_disabled.svg new file mode 100644 index 000000000..8ca9ee7ae --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/vrf_disabled.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/wifi.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/wifi.svg new file mode 100644 index 000000000..845309967 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/wifi.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/wifi_big.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/wifi_big.svg new file mode 100644 index 000000000..a54b82426 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/wifi_big.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/wifi_big_disabled.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/wifi_big_disabled.svg new file mode 100644 index 000000000..48e105321 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/wifi_big_disabled.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/wifi_disabled.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/wifi_disabled.svg new file mode 100644 index 000000000..6f909f259 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/wifi_disabled.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/wireguard.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/wireguard.svg new file mode 100644 index 000000000..b147e0e8f --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/wireguard.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/icons/wireguard_disabled.svg b/luci-theme-liquid/htdocs/luci-static/liquid/icons/wireguard_disabled.svg new file mode 100644 index 000000000..e44451a54 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/icons/wireguard_disabled.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/img/MontereyDark.jpg b/luci-theme-liquid/htdocs/luci-static/liquid/img/MontereyDark.jpg new file mode 100644 index 000000000..4b214746d Binary files /dev/null and b/luci-theme-liquid/htdocs/luci-static/liquid/img/MontereyDark.jpg differ diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/img/MontereyLight.jpg b/luci-theme-liquid/htdocs/luci-static/liquid/img/MontereyLight.jpg new file mode 100644 index 000000000..5868eaf10 Binary files /dev/null and b/luci-theme-liquid/htdocs/luci-static/liquid/img/MontereyLight.jpg differ diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/logo.png b/luci-theme-liquid/htdocs/luci-static/liquid/logo.png new file mode 100644 index 000000000..511981e4e Binary files /dev/null and b/luci-theme-liquid/htdocs/luci-static/liquid/logo.png differ diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/logo.svg b/luci-theme-liquid/htdocs/luci-static/liquid/logo.svg new file mode 100644 index 000000000..f679fe348 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/main.js b/luci-theme-liquid/htdocs/luci-static/liquid/main.js new file mode 100644 index 000000000..480c5855a --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/main.js @@ -0,0 +1,2317 @@ +'use strict'; + +/* Liquid glass theme — light / dark / auto mode switch. + * + * The switch is injected right before the native LuCI refresh / poll + * indicator area (#indicators) in the top bar. On the lock screen + * (sysauth, which has no top bar) it is rendered as a fixed button in + * the top right corner instead. + * + * Modes are persisted in localStorage under "liquid-theme-mode" and the + * effective dark state is reflected on . + */ + +(function () { + if (typeof L == 'undefined' || typeof L.media != 'function') + return; + + var MODE_KEY = 'liquid-theme-mode'; + + /* 主题配置写入 uci /etc/config/liquid(换客户端仍保留): + - 用 LuCI 官方 uci 实例(uci.load + uci.set + uci.save), + 跨 luci 版本最兼容(L.bind 手拼 RPC 在其他固件上不可靠); + uci.save 直接写盘,不经过 ui.changes → 无待应用项、 + 不弹"保存更改"询问,改动立即生效 + - 锁屏(登录页,未认证)不允许修改配置:只做本地预览 + (localStorage),登录后刷新才真正持久化 */ + /* 一次 POST 全部 option(对象 {option: value}),后端一次写盘, + 避免多次请求并发时后写覆盖先写(accent 与 accent_custom 一起存) */ + function saveConfig(opts) { + if (!document.body) + return; + /* 登录页:暂存到 sessionStorage,不发任何请求 */ + if (document.body.classList.contains('liquid-login')) { + try { + var prev = JSON.parse(sessionStorage.getItem('liquid-pending') || '{}'); + var merged = {}; + for (var k in prev) merged[k] = prev[k]; + for (var k2 in opts) merged[k2] = opts[k2]; + sessionStorage.setItem('liquid-pending', JSON.stringify(merged)); + } catch (e) {} + return; + } + /* 正常页面:POST 到 ucode controller 持久化 */ + try { + var base = (window.L && L.env && L.env.admin_path) + ? L.env.admin_path : '/cgi-bin/luci/admin/'; + var xhr = new XMLHttpRequest(); + xhr.open('POST', base + 'system/liquid/save_config'); + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.send(JSON.stringify(opts)); + } catch (e) {} + } + + /* ===== 登录前暂存传递(main.js 顶部立即执行) ===== + sessionStorage('liquid-pending') → localStorage(纯本地写入,极快)。 + header ut 的 inline script 会用 localStorage 值覆盖 uci 旧值。 + 登录页不执行(登录页由 setMode 正常管理)。 */ + (function applyPending() { + if (document.body && document.body.classList.contains('liquid-login')) + return; + try { + var raw = sessionStorage.getItem('liquid-pending'); + if (!raw) return; + var data = JSON.parse(raw); + if (!data || typeof data !== 'object') return; + /* 写 localStorage(header ut 会读到并覆盖 uci 旧值) */ + if (data.mode) localStorage.setItem('liquid-theme-mode', data.mode); + if (data.accent) localStorage.setItem('liquid-accent', data.accent); + if (data.accent_custom) localStorage.setItem('liquid-accent-custom', data.accent_custom); + if (data.bing) localStorage.setItem('liquid-bing', data.bing); + if (data.glass_opacity != null) localStorage.setItem('liquid-glass-opacity', String(data.glass_opacity)); + } catch (e) {} + })(); + + /* 登录后提交暂存(DOMContentLoaded 后异步 POST 持久化,不改 DOM) */ + function flushPending() { + /* flushPending 现在由 showPendingToast 的按钮触发, + 此处保留为空壳,避免登录后自动提交 */ + } + /* 登录后提示暂存设置(DOMContentLoaded 后检查 sessionStorage) */ + function showPendingToast() { + if (document.body && document.body.classList.contains('liquid-login')) + return; + try { + if (!sessionStorage.getItem('liquid-pending')) return; + } catch (e) { return; } + + /* 弹出玻璃 toast 提示 */ + var toast = document.createElement('div'); + toast.className = 'liquid-pending-toast'; + toast.innerHTML = + '登录前修改的主题设置已暂存' + + '' + + '' + + ''; + document.body.appendChild(toast); + requestAnimationFrame(function () { toast.classList.add('show'); }); + + var countdownEl = toast.querySelector('.liquid-pending-toast-countdown'); + var remaining = 30; + function tick() { + if (remaining <= 0) { + dismiss(); + return; + } + countdownEl.textContent = remaining + 's'; + remaining--; + countdownTimer = setTimeout(tick, 1000); + } + var countdownTimer = setTimeout(tick, 1000); + + function applyPending() { + clearTimeout(countdownTimer); + try { + var raw = sessionStorage.getItem('liquid-pending'); + if (raw) { + var data = JSON.parse(raw); + sessionStorage.removeItem('liquid-pending'); + if (data && typeof data === 'object') { + var base = (window.L && L.env && L.env.admin_path) + ? L.env.admin_path : '/cgi-bin/luci/admin/'; + var xhr = new XMLHttpRequest(); + xhr.open('POST', base + 'system/liquid/save_config', true); + xhr.setRequestHeader('Content-Type', 'application/json'); + xhr.send(JSON.stringify(data)); + } + } + } catch (e) {} + setTimeout(function () { location.reload(); }, 200); + } + + function dismiss() { + clearTimeout(countdownTimer); + sessionStorage.removeItem('liquid-pending'); + toast.classList.remove('show'); + setTimeout(function () { toast.remove(); }, 300); + } + + toast.querySelector('[data-action="apply"]').addEventListener('click', applyPending); + toast.querySelector('[data-action="dismiss"]').addEventListener('click', dismiss); + } + + var mql = (typeof window.matchMedia == 'function') + ? window.matchMedia('(prefers-color-scheme: dark)') + : null; + + var MODES = [ + { id: 'light', icon: 'sun.svg', title: 'Light mode' }, + { id: 'dark', icon: 'moon.svg', title: 'Dark mode' }, + { id: 'auto', icon: 'auto.svg', title: 'Follow system' } + ]; + + var ACCENTS = [ + { id: 'blue', color: '#2f7fe0', title: 'Blue' }, + { id: 'magenta', color: '#d63384', title: 'Magenta' }, + { id: 'amber', color: '#e8940f', title: 'Amber' }, + { id: 'purple', color: '#8e44ad', title: 'Tulip purple' }, + { id: 'lime', color: '#7fb52a', title: 'Yellow-green' } + ]; + + var DEFAULT_ACCENT = '#2f7fe0'; + + /* 自定义主题色工具:hex 校验/规范化 + 由单个主色派生全套 accent 变量 */ + function normalizeHex(v) { + v = String(v || '').trim().replace(/^#/, ''); + if (/^[0-9a-fA-F]{3}$/.test(v)) + v = v[0] + v[0] + v[1] + v[1] + v[2] + v[2]; + return /^[0-9a-fA-F]{6}$/.test(v) ? '#' + v.toLowerCase() : null; + } + + function hexToRgb(hex) { + var n = parseInt(hex.slice(1), 16); + return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 }; + } + + function shade(rgb, pct) { + var c = function (v) { + v = Math.round(pct > 0 ? v + (255 - v) * pct : v * (1 + pct)); + return Math.max(0, Math.min(255, v)); + }; + return 'rgb(' + c(rgb.r) + ',' + c(rgb.g) + ',' + c(rgb.b) + ')'; + } + + function rgba(rgb, a) { + return 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + a + ')'; + } + + /* 在 上生成自定义色全套变量(亮/暗两套) */ + function applyCustomAccent(hex) { + hex = normalizeHex(hex) || DEFAULT_ACCENT; + var rgb = hexToRgb(hex); + var root = document.documentElement; + root.style.setProperty('--accent-custom', hex); + root.style.setProperty('--accent-custom-medium', shade(rgb, -0.15)); + root.style.setProperty('--accent-custom-low', shade(rgb, -0.32)); + root.style.setProperty('--accent-custom-glow', rgba(rgb, 0.38)); + root.style.setProperty('--accent-custom-grad', 'linear-gradient(135deg, ' + shade(rgb, 0.25) + ' 0%, ' + hex + ' 48%, ' + shade(rgb, -0.4) + ' 100%)'); + root.style.setProperty('--accent-custom-glass', 'linear-gradient(135deg, ' + rgba(rgb, 0.3) + ', rgba(255,255,255,0.62))'); + root.style.setProperty('--accent-custom-glass-soft', 'linear-gradient(135deg, ' + rgba(rgb, 0.24) + ', rgba(255,255,255,0.72))'); + root.style.setProperty('--accent-custom-dark-high', shade(rgb, 0.3)); + root.style.setProperty('--accent-custom-dark-medium', shade(rgb, 0.12)); + root.style.setProperty('--accent-custom-dark-low', hex); + root.style.setProperty('--accent-custom-dark-glow', rgba(rgb, 0.42)); + root.style.setProperty('--accent-custom-dark-grad', 'linear-gradient(135deg, ' + shade(rgb, 0.5) + ' 0%, ' + shade(rgb, 0.3) + ' 48%, ' + shade(rgb, 0.12) + ' 100%)'); + root.style.setProperty('--accent-custom-dark-glass', 'linear-gradient(135deg, ' + rgba(rgb, 0.28) + ', rgba(46,58,84,0.45))'); + root.style.setProperty('--accent-custom-dark-glass-soft', 'linear-gradient(135deg, ' + rgba(rgb, 0.22) + ', rgba(46,58,84,0.4))'); + } + + function getMode() { + var d = document.body ? document.body.getAttribute('data-liquid-mode') : null; + if (d) + return d; + try { return localStorage.getItem(MODE_KEY) || 'auto'; } catch (e) { return 'auto'; } + } + + function setMode(mode) { + try { localStorage.setItem(MODE_KEY, mode); } catch (e) {} + if (document.body) + document.body.setAttribute('data-liquid-mode', mode); + applyMode(mode); + updateSwitch(); + /* 明暗切换后 accent 背景的 RGB 需要用新模式的色值重写 */ + setGlassOpacity(getGlassOpacity()); + saveConfig({ mode: mode }); + } + + function isDark(mode) { + return (mode == 'dark') || ((mode == 'auto') && mql && mql.matches); + } + + function applyMode(mode) { + var root = document.documentElement; + /* 加 transition 让明暗切换平滑过渡,避免整页重算导致白屏闪烁 */ + root.style.transition = 'background-color .3s ease, color .3s ease'; + root.setAttribute('data-darkmode', isDark(mode) ? 'true' : 'false'); + root.setAttribute('data-liquid-mode', mode); + setTimeout(function () { root.style.transition = ''; }, 350); + } + + /* 明暗模式守护:OpenClash 等第三方脚本会覆写 + (其 ocApplyRootTheme 按背景亮度判定后直接 setAttribute,且默认 + oc-theme=auto),导致一进入 OpenClash 页面主题就被强制切成暗黑。 + 这里用 MutationObserver 监视该属性,任何与主题有效模式不符的外部 + 写入都被立即还原,并顺带同步 meta[name=color-scheme]——主题的 + 明暗由主题自身决定,不受第三方反控制。 */ + var _liquidGuardSelf = false; + function guardDarkmode() { + var root = document.documentElement; + if (root.getAttribute('data-liquid-guard')) + return; + root.setAttribute('data-liquid-guard', '1'); + if (typeof MutationObserver == 'undefined') + return; + /* 还原逻辑:把 data-darkmode / color-scheme 拉回主题有效模式 */ + function enforce() { + var want = isDark(getMode()) ? 'true' : 'false'; + if (root.getAttribute('data-darkmode') !== want) { + _liquidGuardSelf = true; + root.setAttribute('data-darkmode', want); + _liquidGuardSelf = false; + } + var m = document.querySelector('meta[name="color-scheme"]'); + if (m && m.content !== (want == 'true' ? 'dark' : 'light')) + m.content = (want == 'true' ? 'dark' : 'light'); + syncThemeColor(); + } + var obs = new MutationObserver(function () { + if (_liquidGuardSelf) + return; + enforce(); + }); + obs.observe(root, { attributes: true, attributeFilter: ['data-darkmode'] }); + /* 关键:安装监视器时立即校正一次当前状态。OpenClash 的 common.js + 常在 main.js(footer 加载)之前执行,auto 模式下按 body 背景 + 亮度误判成 dark 并已写入 data-darkmode;若只监视后续变化, + 从 main.js 加载到 OpenClash 下次重算之间页面会一直保持暗黑, + 形成"进 OpenClash 先闪暗再纠正"的窗口。footer 脚本在首帧绘制 + 前执行,此刻校正可在用户看到任何暗色之前完成还原。 */ + enforce(); + } + + /* 浏览器工具栏配色(meta theme-color):取底部 foot 玻璃横幅(p.luci) + 的真实渲染色,移动端浏览器工具栏才能与页面上下玻璃栏融为一体。 + foot = --glass-bg-strong 半透明玻璃叠在 body 底色 --background-color-medium + 上;theme-color 要不透明值,用 1x1 canvas 做真实叠加合成(canvas + 原生解析 CSS 色值并完成 alpha 混合),随明暗/配色变量自动跟随, + 不硬编码色值。顶栏 #menubar 与 foot 同为 --glass-bg-strong, + 故该色同时贴合顶部地址栏区域。 */ + function syncThemeColor() { + var meta = document.querySelector('meta[name="theme-color"]'); + if (!meta) + return; + try { + var cs = getComputedStyle(document.documentElement); + var bg = (cs.getPropertyValue('--background-color-medium') || '').trim(); + var glass = (cs.getPropertyValue('--glass-bg-strong') || '').trim(); + if (!bg || !glass) + return; + var c = document.createElement('canvas'); + c.width = 1; + c.height = 1; + var ctx = c.getContext('2d'); + if (!ctx) + return; + ctx.fillStyle = bg; + ctx.fillRect(0, 0, 1, 1); + ctx.fillStyle = glass; + ctx.fillRect(0, 0, 1, 1); + var d = ctx.getImageData(0, 0, 1, 1).data; + var hex = '#'; + for (var i = 0; i < 3; i++) + hex += ('0' + d[i].toString(16)).slice(-2); + if (meta.content !== hex) + meta.content = hex; + } catch (e) {} + } + + function updateSwitch() { + var mode = getMode(); + [].forEach.call(document.querySelectorAll('.liquid-mode-btn'), function (b) { + b.classList.toggle('active', b.getAttribute('data-mode') == mode); + }); + } + + /* ---- accent color switch ---- */ + + function getAccent() { + var d = document.body ? document.body.getAttribute('data-liquid-accent') : null; + if (d) + return d; + try { return localStorage.getItem('liquid-accent') || 'blue'; } catch (e) { return 'blue'; } + } + + function setAccent(id) { + try { localStorage.setItem('liquid-accent', id); } catch (e) {} + if (document.body) + document.body.setAttribute('data-liquid-accent', id); + document.documentElement.setAttribute('data-accent', id); + if (id == 'custom') + applyCustomAccent(getAccentCustom()); + updateColorSwitch(); + if (id != 'custom') + saveConfig({ accent: id }); + } + + function getAccentCustom() { + var d = document.body ? document.body.getAttribute('data-liquid-accent-custom') : null; + if (d) + return d; + try { return localStorage.getItem('liquid-accent-custom') || DEFAULT_ACCENT; } catch (e) { return DEFAULT_ACCENT; } + } + + /* 设置自定义主题色:非法值退回默认蓝,但仍启用自定义模式 */ + function setCustomAccent(hex) { + hex = normalizeHex(hex) || DEFAULT_ACCENT; + try { localStorage.setItem('liquid-accent-custom', hex); } catch (e) {} + if (document.body) + document.body.setAttribute('data-liquid-accent-custom', hex); + setAccent('custom'); + saveConfig({ accent: 'custom', accent_custom: hex }); + } + + function getBing() { + var d = document.body ? document.body.getAttribute('data-liquid-bing') : null; + if (d) + return d; + try { return localStorage.getItem('liquid-bing') || '0'; } catch (e) { return '0'; } + } + + function setBing(on) { + var v = on ? '1' : '0'; + try { localStorage.setItem('liquid-bing', v); } catch (e) {} + if (document.body) { + document.body.setAttribute('data-liquid-bing', v); + /* 页面渲染时模板已确保当日壁纸缓存就绪,打开即视为可用,即时生效 */ + document.body.setAttribute('data-liquid-bing-ok', v); + } + updateColorSwitch(); + saveConfig({ bing: v }); + } + + /* ---- glass opacity slider ---- + + 滑杆 0~100 → alpha 系数 0.00~1.00(0=全透,100=全不透)。 + 默认值 = 现有设计参数值(亮/暗各不同), + 各变量 alpha = baseAlpha × (slider / default), + cap 到 [0, 1] 避免 RGBA 超值。 + 竖线 tick 标记默认位置,点击即回到默认。 */ + + var GLASS_OPACITY_DEFAULT_LIGHT = 42; + var GLASS_OPACITY_DEFAULT_DARK = 40; + + function glassOpacityDefault() { + var dark = document.documentElement.getAttribute('data-darkmode') === 'true'; + return dark ? GLASS_OPACITY_DEFAULT_DARK : GLASS_OPACITY_DEFAULT_LIGHT; + } + + function getGlassOpacity() { + var d = document.body ? document.body.getAttribute('data-liquid-glass-opacity') : null; + if (d && d !== '') + return parseInt(d, 10); + try { return parseInt(localStorage.getItem('liquid-glass-opacity'), 10) || glassOpacityDefault(); } catch (e) { return glassOpacityDefault(); } + } + + function setGlassOpacity(v) { + v = Math.max(0, Math.min(100, Math.round(v))); + try { localStorage.setItem('liquid-glass-opacity', String(v)); } catch (e) {} + if (document.body) + document.body.setAttribute('data-liquid-glass-opacity', String(v)); + var def = glassOpacityDefault(); + var factor = def > 0 ? Math.min(v / def, 100 / def) : 1; + var root = document.documentElement; + root.style.setProperty('--glass-opacity', factor.toFixed(2)); + /* 直接重写 accent 背景色(JS 计算 rgba,比 CSS calc 在渐变里更可靠) */ + _applyAccentAlpha(factor); + } + + function _applyAccentAlpha(f) { + var root = document.documentElement; + var cs = getComputedStyle(root); + var clamp01 = function (x) { return Math.max(0, Math.min(1, x)); }; + var a = function (varName, base) { + /* 从 CSS 变量读 base alpha(第 4 个参数),乘以 factor,clamp */ + return clamp01(base * f); + }; + /* 当前主题色的 accent-glass 变量基准 alpha(亮/暗通用): + 取自计算后的变量值,直接重写 */ + var accentHigh = cs.getPropertyValue('--primary-color-high').trim() || '#2f7fe0'; + /* accent-glass / accent-glass-soft / accent-glow 的亮暗基准值 */ + var dark = root.getAttribute('data-darkmode') === 'true'; + var glowBase = dark ? 0.40 : 0.38; + var glassBase = dark ? [0.30, 0.50] : [0.32, 0.62]; + var softBase = dark ? [0.24, 0.45] : [0.26, 0.72]; + var glowA = a('--accent-glow-base', glowBase); + var glassA1 = a('--accent-glass-base', glassBase[0]); + var glassA2 = a('--accent-glass2-base', glassBase[1]); + var softA1 = a('--accent-soft-base', softBase[0]); + var softA2 = a('--accent-soft2-base', softBase[1]); + root.style.setProperty('--accent-glow', 'rgba(' + _hexToRgb(accentHigh) + ',' + glowA.toFixed(2) + ')'); + root.style.setProperty('--accent-glass', + 'linear-gradient(135deg, rgba(' + _hexToRgb(accentHigh) + ',' + glassA1.toFixed(2) + '), ' + + (dark ? 'rgba(46,58,84,' + glassA2.toFixed(2) + ')' : 'rgba(255,255,255,' + glassA2.toFixed(2) + ')') + ')'); + root.style.setProperty('--accent-glass-soft', + 'linear-gradient(135deg, rgba(' + _hexToRgb(accentHigh) + ',' + softA1.toFixed(2) + '), ' + + (dark ? 'rgba(46,58,84,' + softA2.toFixed(2) + ')' : 'rgba(255,255,255,' + softA2.toFixed(2) + ')') + ')'); + } + + function _hexToRgb(hex) { + hex = (hex || '#2f7fe0').replace('#', ''); + if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2]; + return parseInt(hex.substring(0,2),16) + ',' + parseInt(hex.substring(2,4),16) + ',' + parseInt(hex.substring(4,6),16); + } + + function updateGlassSlider() { + var s = document.querySelector('.liquid-glass-slider'); + if (s) + s.value = getGlassOpacity(); + } + + function initGlassOpacitySlider() { + var sw = document.querySelector('.liquid-color-switch'); + if (!sw || sw.querySelector('.liquid-glass-slider-wrap')) + return; + + var wrap = document.createElement('div'); + wrap.className = 'liquid-glass-slider-wrap'; + + var slider = document.createElement('input'); + slider.type = 'range'; + slider.className = 'liquid-glass-slider'; + slider.min = '0'; + slider.max = '100'; + slider.step = '1'; + slider.value = String(getGlassOpacity()); + slider.title = 'Glass opacity'; + slider.setAttribute('aria-label', 'Glass opacity'); + + var def = glassOpacityDefault(); + + /* 计算 slider thumb 中心相对于 wrap 左边缘的像素位置 */ + function thumbLeftPx(value) { + var slRect = slider.getBoundingClientRect(); + var wRect = wrap.getBoundingClientRect(); + /* thumb 的中心偏移 = slider 内容区 left + border + thumb半径 + value比例 × 可用宽度 */ + var cs = getComputedStyle(slider); + var padL = parseFloat(cs.paddingLeft) || 0; + var padR = parseFloat(cs.paddingRight) || 0; + var bw = parseFloat(cs.borderLeftWidth) || 0; + var trackW = slRect.width - padL - padR - bw * 2; + var thumbHalf = 8; /* thumb 宽 16px,半径 8 */ + var px = (slRect.left - wRect.left) + bw + padL + thumbHalf + (value / 100) * (trackW - thumbHalf * 2); + return px; + } + + /* 浮动数值气泡:拖动时跟随 thumb 实时显示当前值 */ + var bubble = document.createElement('div'); + bubble.className = 'liquid-glass-slider-bubble'; + bubble.style.display = 'none'; + wrap.appendChild(bubble); + + function updateBubble() { + var v = parseInt(slider.value, 10); + bubble.textContent = v; + bubble.style.left = thumbLeftPx(v) + 'px'; + } + + slider.addEventListener('input', function () { + setGlassOpacity(parseInt(slider.value, 10)); + updateBubble(); + }); + slider.addEventListener('pointerdown', function () { + bubble.style.display = ''; + updateBubble(); + }); + slider.addEventListener('pointerup', function () { + bubble.style.display = 'none'; + saveConfig({ glass_opacity: parseInt(slider.value, 10) }); + }); + slider.addEventListener('pointercancel', function () { + bubble.style.display = 'none'; + }); + + /* 默认值标记:百分比定位(border:none 后 thumb 百分比与 left 百分比一致) */ + var tick = document.createElement('div'); + tick.className = 'liquid-glass-slider-tick'; + tick.title = 'Default'; + tick.style.left = def + '%'; + tick.addEventListener('click', function () { + slider.value = String(def); + setGlassOpacity(def); + saveConfig({ glass_opacity: def }); + }); + + wrap.appendChild(slider); + wrap.appendChild(tick); + + /* 锁屏页:滑杆放在胶囊容器外部下方,宽度跟随胶囊总宽 */ + var loginCapsules = document.getElementById('liquid-login-capsules'); + if (loginCapsules) { + wrap.classList.add('liquid-glass-slider-login'); + loginCapsules.parentNode.insertBefore(wrap, loginCapsules.nextSibling); + /* 动态计算滑杆宽度 = 胶囊实际宽度 - 缩进 */ + function syncSliderWidth() { + var cw = loginCapsules.offsetWidth; + if (cw > 0) + wrap.style.width = Math.max(40, cw - 50) + 'px'; + } + setTimeout(syncSliderWidth, 50); + window.addEventListener('resize', syncSliderWidth); + } else { + sw.appendChild(wrap); + } + } + + function updateColorSwitch() { + var accent = getAccent(); + [].forEach.call(document.querySelectorAll('.liquid-color-btn'), function (b) { + b.classList.toggle('active', b.getAttribute('data-accent') == accent); + }); + [].forEach.call(document.querySelectorAll('.liquid-bing-btn'), function (b) { + b.classList.toggle('active', getBing() == '1'); + }); + } + + function initColorSwitch() { + if (document.getElementById('liquid-color-switch')) + return; + + var wrap = document.createElement('span'); + wrap.id = 'liquid-color-switch'; + wrap.className = 'liquid-color-switch'; + wrap.title = 'Accent color'; + + /* Bing 每日壁纸开关(五色之前):点亮启用在线壁纸 */ + var bingBtn = document.createElement('button'); + bingBtn.type = 'button'; + bingBtn.className = 'liquid-color-btn liquid-bing-btn'; + bingBtn.title = 'Bing 每日壁纸'; + bingBtn.setAttribute('aria-label', 'Bing daily wallpaper'); + bingBtn.innerHTML = ''; + bingBtn.addEventListener('click', function () { + setBing(getBing() != '1'); + }); + wrap.appendChild(bingBtn); + + ACCENTS.forEach(function (c) { + var btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'liquid-color-btn liquid-accent-' + c.id; + btn.title = c.title; + btn.setAttribute('data-accent', c.id); + btn.setAttribute('aria-label', c.title); + btn.style.background = c.color; + btn.addEventListener('click', function () { setAccent(c.id); }); + wrap.appendChild(btn); + }); + + /* 自定义主题色(5 色之后):彩虹圆点,点击下拉输入框输入颜色编码 */ + var customBtn = document.createElement('button'); + customBtn.type = 'button'; + customBtn.className = 'liquid-color-btn liquid-custom-btn'; + customBtn.title = 'Custom color'; + customBtn.setAttribute('data-accent', 'custom'); + customBtn.setAttribute('aria-label', 'Custom color'); + wrap.appendChild(customBtn); + + /* 菜单搜索按钮(Bing 之后):点击下推抽屉式搜索框,实时过滤 + 菜单项(支持中英文标题),再点或点外部收回 */ + var searchBtn = document.createElement('button'); + searchBtn.type = 'button'; + searchBtn.className = 'liquid-color-btn liquid-search-btn'; + searchBtn.title = 'Search menu'; + searchBtn.setAttribute('aria-label', 'Search menu'); + searchBtn.innerHTML = ''; + searchBtn.addEventListener('click', function (e) { + e.stopPropagation(); + toggleMenuSearch(e.currentTarget); + }); + wrap.appendChild(searchBtn); + + var pop = document.createElement('div'); + pop.id = 'liquid-custom-pop'; + pop.className = 'liquid-custom-pop'; + pop.style.display = 'none'; + var input = document.createElement('input'); + input.type = 'text'; + input.className = 'liquid-custom-input'; + input.placeholder = '#RRGGBB'; + input.maxLength = 7; + input.setAttribute('spellcheck', 'false'); + pop.appendChild(input); + wrap.appendChild(pop); + + function toggleCustomPop() { + var show = pop.style.display == 'none'; + pop.style.display = show ? 'block' : 'none'; + if (show) { + input.value = getAccentCustom(); + setTimeout(function () { input.focus(); input.select(); }, 0); + } + } + + /* 离开输入框(blur)即保存;非法值退回默认蓝 */ + function commitCustom() { + var hex = normalizeHex(input.value); + if (!hex) + hex = DEFAULT_ACCENT; + setCustomAccent(hex); + } + + customBtn.addEventListener('click', function (e) { + e.stopPropagation(); + toggleCustomPop(); + }); + input.addEventListener('keydown', function (e) { + if (e.key == 'Enter') { + commitCustom(); + pop.style.display = 'none'; + } + else if (e.key == 'Escape') { + pop.style.display = 'none'; + } + }); + input.addEventListener('blur', function () { commitCustom(); }); + document.addEventListener('click', function (e) { + if (!wrap.contains(e.target)) + pop.style.display = 'none'; + }); + + var menubar = document.querySelector('#mainmenu'); + + if (menubar) { + /* 菜单栏顶部(水平居中) */ + menubar.insertBefore(wrap, menubar.firstChild); + } + else { + /* lock screen / pages without a menubar */ + var capsules = document.getElementById('liquid-login-capsules'); + if (capsules) { + /* 登录页:插入胶囊容器,不加 fixed 定位 */ + capsules.appendChild(wrap); + } + else { + wrap.classList.add('liquid-color-switch-fixed'); + document.body.appendChild(wrap); + } + } + + updateColorSwitch(); + } + + /* ── 菜单搜索(抽屉式下推)──────────────────────────── + 搜索按钮点击:在 #mainmenu 顶部下推一个圆角矩形搜索框, + 下面的菜单整体被往下推(抽屉式,非悬浮)。输入时实时过滤 + 菜单项(匹配显示文本 + 英文原题 data-title)。再次点按钮 + 或点击菜单外区域收回。 */ + var menuSearchOpen = false; + var menuSearchBox = null; + var menuSearchInput = null; + + function buildMenuSearchBox() { + menuSearchBox = document.createElement('div'); + menuSearchBox.id = 'liquid-menu-search'; + menuSearchBox.className = 'liquid-menu-search'; + + var icon = document.createElement('span'); + icon.className = 'liquid-menu-search-icon'; + icon.innerHTML = ''; + + menuSearchInput = document.createElement('input'); + menuSearchInput.type = 'text'; + menuSearchInput.className = 'liquid-menu-search-input'; + menuSearchInput.placeholder = '搜索 / Search'; + menuSearchInput.setAttribute('spellcheck', 'false'); + menuSearchInput.addEventListener('input', function () { + filterMenuItems(); + updateClearBtn(); + }); + + var clearBtn = document.createElement('button'); + clearBtn.type = 'button'; + clearBtn.className = 'liquid-menu-search-clear'; + clearBtn.innerHTML = '×'; + clearBtn.title = 'Clear'; + clearBtn.addEventListener('click', function () { + menuSearchInput.value = ''; + filterMenuItems(); + updateClearBtn(); + menuSearchInput.focus(); + }); + + function updateClearBtn() { + clearBtn.style.display = menuSearchInput.value ? 'inline-flex' : 'none'; + } + updateClearBtn(); + + menuSearchBox.appendChild(icon); + menuSearchBox.appendChild(menuSearchInput); + menuSearchBox.appendChild(clearBtn); + + var menu = document.getElementById('mainmenu'); + if (menu) { + /* 插在颜色胶囊之后(本功能区不动,搜索框从其下方推出) */ + var sw = menu.querySelector('.liquid-color-switch'); + if (sw && sw.nextSibling) + menu.insertBefore(menuSearchBox, sw.nextSibling); + else + menu.insertBefore(menuSearchBox, menu.firstChild); + } + else + document.body.appendChild(menuSearchBox); + + return menuSearchBox; + } + + function filterMenuItems() { + var q = (menuSearchInput.value || '').trim().toLowerCase(); + var menu = document.getElementById('mainmenu'); + if (!menu) + return; + + /* 收集所有菜单项(一级/二级,含中英文标题) */ + var items = []; + menu.querySelectorAll('ul.mainmenu li').forEach(function (li) { + var a = li.querySelector(':scope > a'); + if (!a) + return; + var isL1 = li.parentElement && li.parentElement.classList.contains('l1'); + items.push({ + li: li, + text: (a.textContent || '').trim().toLowerCase(), + en: ((a.getAttribute('data-title') || '')).toLowerCase(), + isL1: isL1 + }); + }); + + /* 清空:恢复全部显示 + 还原展开状态(只留当前页所在菜单) */ + if (!q) { + items.forEach(function (it) { + it.li.classList.remove('liquid-menu-hidden'); + }); + menu.querySelectorAll('ul.mainmenu.l1 > li').forEach(function (li) { + if (!li.classList.contains('selected')) + li.classList.remove('active'); + }); + return; + } + + /* 匹配判断 */ + items.forEach(function (it) { + it.match = (it.text.indexOf(q) != -1 || it.en.indexOf(q) != -1); + }); + + /* 按一级菜单分组处理: + 一级匹配 → 展开并显示其全部二级项; + 二级匹配 → 展开所属一级,显示匹配的二级项; + 均不匹配 → 整组隐藏 */ + menu.querySelectorAll('ul.mainmenu.l1 > li').forEach(function (li) { + var l1 = null, l2s = []; + items.forEach(function (it) { + if (it.li === li) + l1 = it; + else if (!it.isL1 && li.contains(it.li)) + l2s.push(it); + }); + var l1Match = l1 && l1.match; + var hasL2Match = l2s.some(function (it) { return it.match; }); + + if (l1Match || hasL2Match) { + li.classList.remove('liquid-menu-hidden'); + li.classList.add('active'); /* 展开二级菜单 */ + l2s.forEach(function (it) { + if (l1Match || it.match) + it.li.classList.remove('liquid-menu-hidden'); + else + it.li.classList.add('liquid-menu-hidden'); + }); + } + else { + li.classList.add('liquid-menu-hidden'); + } + }); + } + + function toggleMenuSearch(btn) { + if (menuSearchOpen) { + closeMenuSearch(); + return; + } + + if (!menuSearchBox) + buildMenuSearchBox(); + + menuSearchBox.classList.add('open'); + menuSearchOpen = true; + var menu = document.getElementById('mainmenu'); + if (menu) + menu.classList.add('liquid-search-open'); + /* 搜索按钮与其他圆点一致的 active 高亮(用事件源按钮,可靠) */ + if (btn) + btn.classList.add('active'); + else { + var b = document.querySelector('.liquid-search-btn'); + if (b) + b.classList.add('active'); + } + setTimeout(function () { menuSearchInput.focus(); }, 50); + } + + function closeMenuSearch() { + if (!menuSearchOpen) + return; + menuSearchOpen = false; + if (menuSearchBox) + menuSearchBox.classList.remove('open'); /* height/opacity transition 平滑收回 */ + var menu = document.getElementById('mainmenu'); + if (menu) + menu.classList.remove('liquid-search-open'); + /* 搜索按钮还原 */ + var btn = document.querySelector('.liquid-search-btn'); + if (btn) + btn.classList.remove('active'); + /* 清空过滤,恢复全部菜单 */ + if (menuSearchInput) { + menuSearchInput.value = ''; + filterMenuItems(); + } + /* 还原菜单展开状态:只保留当前页所在的一级菜单展开 */ + if (menu) { + menu.querySelectorAll('ul.mainmenu.l1 > li').forEach(function (li) { + if (!li.classList.contains('selected')) + li.classList.remove('active'); + }); + } + } + + /* 点击菜单外区域收回(document 捕获阶段,排除搜索框/按钮自身) */ + document.addEventListener('click', function (e) { + if (!menuSearchOpen) + return; + if (menuSearchBox && menuSearchBox.contains(e.target)) + return; + if (e.target.closest && e.target.closest('.liquid-search-btn')) + return; + closeMenuSearch(); + }, true); + + function initSwitch() { + if (document.getElementById('liquid-mode-switch')) + return; + + var wrap = document.createElement('span'); + wrap.id = 'liquid-mode-switch'; + wrap.className = 'liquid-mode-switch'; + + MODES.forEach(function (m) { + var btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'liquid-mode-btn liquid-mode-' + m.id; + btn.title = m.title; + btn.setAttribute('data-mode', m.id); + btn.setAttribute('aria-label', m.title); + + var img = document.createElement('img'); + img.src = L.media('svg/' + m.icon); + img.alt = ''; + img.draggable = false; + + btn.appendChild(img); + btn.addEventListener('click', function () { setMode(m.id); }); + wrap.appendChild(btn); + }); + + var indicators = document.getElementById('indicators'); + + if (indicators && indicators.parentNode) { + /* 顶栏原位置(刷新指示区左侧) */ + indicators.parentNode.insertBefore(wrap, indicators); + } + else { + /* lock screen / pages without a menubar */ + var capsules = document.getElementById('liquid-login-capsules'); + if (capsules) { + /* 登录页:插入胶囊容器,不加 fixed 定位 */ + capsules.appendChild(wrap); + } + else { + wrap.classList.add('liquid-mode-switch-fixed'); + document.body.appendChild(wrap); + } + } + + updateSwitch(); + } + + /* keep auto mode in sync with OS theme changes */ + if (mql && typeof mql.addEventListener == 'function') { + mql.addEventListener('change', function () { + if (getMode() == 'auto') + applyMode('auto'); + }); + } + else if (mql && typeof mql.addListener == 'function') { + mql.addListener(function () { + if (getMode() == 'auto') + applyMode('auto'); + }); + } + + /* 修复 zh-cn 复数翻译缺失:多个网络时 "Part of networks:" 显示英文原文, + 统一成中文 */ + if (typeof N_ == 'function') { + var origN = N_; + N_ = function (count, one, other) { + if (one == 'Part of network:' && other == 'Part of networks:') + return '网络的一部分:'; + return origN(count, one, other); + }; + } + + /* 内容区 tab 菜单追踪滑块(跟随 hover/选中项平滑滑动) */ + function initTabSliders() { + document.querySelectorAll('ul.cbi-tabmenu').forEach(function (ul) { + if (ul.classList.contains('liquid-tab-slider-init')) + return; + + ul.classList.add('liquid-tab-slider-init'); + + var ind = document.createElement('span'); + ind.className = 'liquid-tab-indicator'; + ul.appendChild(ind); + + function moveTo(el) { + if (!el || !ul.contains(el)) { + ind.style.opacity = '0'; + return; + } + var r = el.getBoundingClientRect(); + var ur = ul.getBoundingClientRect(); + ind.style.width = r.width + 'px'; + ind.style.height = r.height + 'px'; + ind.style.transform = 'translate(' + (r.left - ur.left) + 'px, ' + (r.top - ur.top) + 'px)'; + ind.style.opacity = '1'; + } + + moveTo(ul.querySelector('li.cbi-tab')); + + ul.addEventListener('mouseover', function (e) { + var li = e.target.closest ? e.target.closest('li') : null; + if (li && ul.contains(li)) + moveTo(li); + }); + ul.addEventListener('mouseleave', function () { + moveTo(ul.querySelector('li.cbi-tab')); + }); + + if (window.MutationObserver) { + var mo = new MutationObserver(function () { + moveTo(ul.querySelector('li.cbi-tab')); + }); + mo.observe(ul, { attributes: true, subtree: true, attributeFilter: ['class'] }); + } + }); + } + + /* 下拉宽度自适应:取选项中最长文本宽度 + 余量,总宽上限 240px + ("保存并应用"组合按钮除外,保持 auto)。原生 select 同步。 */ + function fitDropdownWidths() { + document.querySelectorAll('.cbi-dropdown:not(.cbi-button-apply)').forEach(function (dd) { + if (dd.classList.contains('liquid-dd-fit')) + return; + /* 打开中的下拉跳过测量:LuCI 正在克隆 preview/聚焦输入, + 此时改 li/img 样式会干扰打开与自定义输入流程 */ + if (dd.hasAttribute('open') || dd.classList.contains('open')) + return; + dd.classList.add('liquid-dd-fit'); + var ul = dd.querySelector('ul'); + if (!ul) + return; + var maxW = 0; + [].forEach.call(ul.querySelectorAll('li'), function (li) { + if (li.classList.contains('hide-close') || li.classList.contains('hide-open')) + return; + /* 隐藏 li 无法直接测宽:临时 absolute+hidden 测量。 + 图标(img)未加载时宽度为 0 会漏算:临时给 24px 估算 */ + var imgs = li.querySelectorAll('img'); + var saved = []; + [].forEach.call(imgs, function (img, i) { + saved[i] = img.style.width; + img.style.width = '24px'; + img.style.flexShrink = '0'; + }); + var st = li.style; + st.display = 'block'; + st.position = 'absolute'; + st.visibility = 'hidden'; + st.whiteSpace = 'nowrap'; + var w = li.scrollWidth || 0; + st.display = ''; + st.position = ''; + st.visibility = ''; + st.whiteSpace = ''; + [].forEach.call(imgs, function (img, i) { + img.style.width = saved[i]; + img.style.flexShrink = ''; + }); + if (w > maxW) + maxW = w; + }); + + /* 初始加载(读 uci 已选中项):当前值行 li[display] 是闭合时 + 唯一可见的项,实测其渲染宽(含图标),保证胶囊不短于已选项 */ + [].forEach.call(ul.querySelectorAll('li[display]'), function (li) { + var imgs = li.querySelectorAll('img'); + var saved = []; + [].forEach.call(imgs, function (img, i) { + saved[i] = img.style.width; + img.style.width = '24px'; + img.style.flexShrink = '0'; + }); + var st = li.style; + st.position = 'absolute'; + st.visibility = 'hidden'; + st.whiteSpace = 'nowrap'; + var w = li.scrollWidth || 0; + st.position = ''; + st.visibility = ''; + st.whiteSpace = ''; + [].forEach.call(imgs, function (img, i) { + img.style.width = saved[i]; + img.style.flexShrink = ''; + }); + if (w > maxW) + maxW = w; + }); + if (maxW > 0) { + var w = Math.min(Math.max(maxW + 40, 60), 240); + dd.style.width = w + 'px'; + dd.dataset.liquidW = w; /* 记录,关闭后恢复(luci 会清) */ + } + }); + + document.querySelectorAll('.cbi-select').forEach(function (sel) { + if (sel.classList.contains('liquid-dd-fit')) + return; + sel.classList.add('liquid-dd-fit'); + var s = sel.querySelector('select'); + if (!s) + return; + var maxW = 0; + [].forEach.call(s.options, function (o) { + var w = (o.text || '').length * 7.5; + if (w > maxW) + maxW = w; + }); + if (maxW > 0) + sel.style.width = Math.min(Math.max(maxW + 60, 60), 240) + 'px'; + }); + } + + /* 下拉框避让:按"屏幕上下可用空间"全局计算(而非卡片内相对位置): + 哪一侧剩余空间更多就朝哪侧弹出;并给列表设动态 max-height, + 使其始终完整落在屏幕内(超出部分出滚动条,不再被屏幕边缘裁掉) */ + function adjustDropdownDirection() { + document.querySelectorAll('.cbi-dropdown').forEach(function (dd) { + var ul = dd.querySelector('ul.dropdown') || dd.querySelector('ul:not(.preview)'); + if (!ul) + return; + /* 打开判定:优先 dd 的 open 状态;部分第三方渲染(如 + OpenClash)不设 open 属性,改判 ul 是否带 dropdown class */ + var open = dd.classList.contains('open') || dd.hasAttribute('open') + || ul.classList.contains('dropdown'); + + if (!open) { + /* 关闭:清理 luci 开合流程残留的 inline 定位(触屏分支的 + left/right 会把闭合胶囊横向拉长)并恢复 fit 宽度 + (luci closeDropdown 会清 dd.style.width) */ + ul.style.left = ''; + ul.style.right = ''; + ul.style.top = ''; + ul.style.bottom = ''; + ul.style.maxHeight = ''; + releaseCardBlur(); + if (dd.dataset.liquidW) + dd.style.width = dd.dataset.liquidW + 'px'; + return; + } + + /* 打开时暂时解除卡片链的 backdrop-filter:玻璃卡片形成 + stacking context + containing block,内部 z-index 9999 + 出不去、fixed/absolute 被限制在卡片内(下拉被遮挡的根因)。 + 移除后下拉 z-index 可盖住后续卡片;关闭时 releaseCardBlur + 恢复玻璃。 */ + holdCardBlur(dd); + + /* 清掉 luci 触屏分支残留的 left/right inline 定位:否则选项 + 列表可能被压成短短一横条/错位(F12 触发 resize 重算才恢复) */ + ul.style.left = ''; + ul.style.right = ''; + + /* 打开时校准宽度:选项实际渲染宽(含图标,无论 img/背景), + 修正初始 fit 对图标漏算导致的"图标被截" */ + var maxW = 0; + ul.querySelectorAll('li').forEach(function (li) { + var w = li.offsetWidth || 0; + if (w > maxW) + maxW = w; + }); + if (maxW > 0) { + var nw = Math.min(Math.max(maxW + 40, 60), 240); + dd.style.width = nw + 'px'; + dd.dataset.liquidW = nw; + } + + var r = dd.getBoundingClientRect(); + var vh = window.innerHeight; + var downSpace = vh - r.bottom; /* 按钮底 → 屏幕底 */ + var upSpace = r.top; /* 屏幕顶 → 按钮顶 */ + var maxH; + + /* 先量出内容完整高度(临时去掉 max-height 限制) */ + ul.style.maxHeight = ''; + var fullH = ul.offsetHeight || 220; + + if (downSpace >= upSpace) { + /* 下方空间更多:向下弹出,上限 = 下方可用空间 */ + ul.style.top = 'calc(100% + 4px)'; + ul.style.bottom = 'auto'; + maxH = Math.max(60, downSpace - 8); + } else { + /* 上方空间更多:向上弹出,上限 = 上方可用空间 */ + ul.style.top = 'auto'; + ul.style.bottom = 'calc(100% + 4px)'; + maxH = Math.max(60, upSpace - 8); + } + + if (fullH > maxH) + ul.style.maxHeight = maxH + 'px'; + else + ul.style.maxHeight = ''; + }); + } + + /* ── 卡片 backdrop-filter 临时解除(下拉不被遮挡的关键)── + 玻璃卡片 backdrop-filter 创建 stacking context + containing + block:内部下拉 z-index 9999 出不去、被后续卡片盖住。打开下拉 + 时给带 backdrop-filter 的祖先加 .liquid-dd-bf-off(CSS 置 + backdrop-filter:none),关闭时移除。 */ + var _bfChain = []; + function holdCardBlur(dd) { + releaseCardBlur(); + var el = dd.parentElement; + while (el && el !== document.body) { + if (getComputedStyle(el).backdropFilter !== 'none' || + getComputedStyle(el).webkitBackdropFilter !== 'none') { + el.classList.add('liquid-dd-bf-off'); + _bfChain.push(el); + } + el = el.parentElement; + } + } + function releaseCardBlur() { + _bfChain.forEach(function (el) { + el.classList.remove('liquid-dd-bf-off'); + }); + _bfChain = []; + } + + /* cbi-dropdown:选择后确保 li[display] 跟随选中项(当前值显示兜底) */ + function syncDropdownValues() { + document.querySelectorAll('.cbi-dropdown').forEach(function (dd) { + if (dd.classList.contains('liquid-dd-init')) + return; + dd.classList.add('liquid-dd-init'); + + function sync() { + var ul = dd.querySelector('ul') || dd._liquidUl; + if (!ul) + return; + + /* 复选框与 selected 状态同步(luci 偶发只改其一) */ + ul.querySelectorAll('li').forEach(function (li) { + var cb = li.querySelector('input[type="checkbox"]'); + if (cb) + cb.checked = li.hasAttribute('selected'); + }); + + var open = dd.classList.contains('open') || dd.hasAttribute('open'); + if (open) + return; /* 打开(多选打勾)时胶囊不变,避免频繁渲染 */ + + if (dd.hasAttribute('multiple')) { + /* 关闭后:把全部选中项设为 display 徽章(解除 luci 默认 + 只显示前几个的限制),竖向展示在胶囊内 */ + var n = 0; + ul.querySelectorAll('li[display]').forEach(function (l) { + if (!l.hasAttribute('selected')) + l.removeAttribute('display'); + }); + ul.querySelectorAll('li[selected]').forEach(function (l) { + if (!l.hasAttribute('display')) + l.setAttribute('display', n); + n++; + }); + return; + } + + var sel = ul.querySelector('li[selected]'); + var cur = ul.querySelector('li[display]'); + if (sel && cur !== sel) { + if (cur) + cur.removeAttribute('display'); + sel.setAttribute('display', '0'); + } + } + + sync(); + /* luci 每次选值/取消都会派发 cbi-dropdown-change:同步勾选框与 + selected 状态(点击即生效,不依赖 observer 时序) */ + dd.addEventListener('cbi-dropdown-change', function () { + var u = dd.querySelector('ul') || dd._liquidUl; + if (!u) + return; + u.querySelectorAll('li').forEach(function (li) { + var cb = li.querySelector('input[type="checkbox"]'); + if (cb) + cb.checked = li.hasAttribute('selected'); + }); + }); + if (window.MutationObserver) { + var mo = new MutationObserver(function () { + sync(); + adjustDropdownDirection(); + }); + mo.observe(dd, { attributes: true, subtree: true, attributeFilter: ['class', 'display', 'open', 'selected'] }); + } + + /* 兜底:点击选项后(ui.js toggleItem/closeDropdown 之后), + 若关闭流程因任何原因中断(如 preview 缺失使 closeDropdown 抛错、 + open 属性残留),强制把下拉重置为闭合态并让选中值显示在框里。 */ + dd.addEventListener('click', function (e) { + /* 多选:luci 保持打开以便连续勾选,兜底不强制关闭; + 但立即同步勾选框(当前事件循环结束、luci 处理完后) */ + if (dd.hasAttribute('multiple')) { + setTimeout(function () { + var u = dd.querySelector('ul') || dd._liquidUl; + if (!u) + return; + u.querySelectorAll('li').forEach(function (li) { + var cb = li.querySelector('input[type="checkbox"]'); + if (cb) + cb.checked = li.hasAttribute('selected'); + }); + }, 0); + return; + } + var li = e.target.closest ? e.target.closest('li') : null; + if (!li || !li.parentNode || !li.parentNode.classList.contains('dropdown')) + return; + /* 点击"自定义"输入行(unselectable / 含 create 输入框)时 + 不能强制关闭——那是输入框,需要滞留让用户输入 */ + if (li.hasAttribute('unselectable') || li.querySelector('.create-item-input')) + return; + setTimeout(function () { + var ul = dd.querySelector('ul.dropdown'); + if (!ul) + return; /* ui.js 已正常关闭 */ + var pv = dd.querySelector('ul.preview'); + if (pv && pv.parentNode === dd) + dd.removeChild(pv); + ul.classList.remove('dropdown'); + ul.style.top = ul.style.bottom = ul.style.maxHeight = ''; + dd.removeAttribute('open'); + var sel = ul.querySelector('li[selected]'); + if (!sel) + return; /* 点击未生效(toggleItem 没跑),不动现状 */ + ul.querySelectorAll('li[display]').forEach(function (l) { + if (l !== sel) + l.removeAttribute('display'); + }); + if (!sel.hasAttribute('display')) + sel.setAttribute('display', '0'); + }, 0); + }); + }); + } + + /* 悬浮内容框(.cbi-tooltip):hover 时把 tooltip 临时移到 body 顶层 + (fixed 定位),彻底脱离卡片 backdrop-filter 的 stacking context, + 这样 tooltip 不会被相邻卡片遮挡。移出后放回原容器。 + 互斥保底:新的 hover 弹出时,先隐藏页面上所有其他 portal tooltip, + 避免 mouseleave 偶尔不触发导致的残留停留。 */ + function portalTooltips() { + document.querySelectorAll('.cbi-tooltip-container').forEach(function (c) { + if (c.classList.contains('liquid-tip-init')) + return; + c.classList.add('liquid-tip-init'); + var tip = c.querySelector('.cbi-tooltip'); + if (!tip) + return; + var origParent = tip.parentNode; + + function hide() { + if (tip.parentNode !== origParent) + origParent.appendChild(tip); + tip.classList.remove('liquid-tip-ported'); + tip.style.position = ''; + tip.style.left = ''; + tip.style.top = ''; + tip.style.zIndex = ''; + tip.style.opacity = '0'; + tip.style.visibility = 'hidden'; + tip.style.pointerEvents = ''; + } + + function show() { + /* 互斥保底:先隐藏页面上所有其他已显示的 portal tooltip */ + document.querySelectorAll('.liquid-tip-ported').forEach(function (other) { + if (other !== tip) { + other.style.opacity = '0'; + other.style.visibility = 'hidden'; + other.classList.remove('liquid-tip-ported'); + } + }); + var r = c.getBoundingClientRect(); + document.body.appendChild(tip); + tip.style.position = 'fixed'; + tip.style.opacity = '0'; + tip.style.visibility = 'visible'; + tip.style.pointerEvents = 'none'; + /* 自动避让:量出悬浮框尺寸,超出屏幕时翻转/收进视口, + 保证完整显示(同一帧内完成,无闪烁) */ + var tw = tip.offsetWidth, th = tip.offsetHeight; + var vw = window.innerWidth, vh = window.innerHeight; + var left = r.left, top = r.bottom + 6; + if (left + tw > vw - 8) + left = Math.max(8, vw - tw - 8); + if (top + th > vh - 8) + top = r.top - th - 6; + if (top < 8) top = 8; + if (left < 8) left = 8; + tip.style.left = left + 'px'; + tip.style.top = top + 'px'; + tip.style.zIndex = '99999'; + tip.style.opacity = '1'; + tip.style.visibility = 'visible'; + tip.classList.add('liquid-tip-ported'); + } + + c.addEventListener('mouseenter', show); + c.addEventListener('mouseleave', hide); + }); + } + + /* 第三方插件的 fixed 弹窗(easytier .version-modal:刷新版本/重启服务 + 确认框):弹窗自身 position:fixed + inset:0 + flex 居中,本应相对 + 视口居中,但插在玻璃卡片内时,卡片的 backdrop-filter 会成为 fixed + 的包含块,弹窗变成相对整张卡片居中——卡片很高时弹窗落在当前屏幕 + 之外(移动端要下划才能看到)。移到 body 下即恢复视口居中。 + 与 portalTooltips 同思路;easytier 用 getElementById + classList + 切换 .show 显隐,移动节点不影响其逻辑。弹窗依赖的 --card-bg 等 + 变量定义在 :root,移出卡片不失效 */ + /* 页面顶部的成功通知(如"系统密码已更改")LuCI 的 ui.addNotification + 用 info 类、插在 #maincontent 顶部(#view 的兄弟节点)——视觉上 + 被内容淹没。这里把它们搬进 #modal_overlay 走既有的居中弹窗体系 + (centerModals/watchModals 已在运行),点击或 6 秒后自动关闭。 + 只搬 info/notice/success(成功提示),danger/warning 留在原位。 */ + function portalTopNotices() { + /* 页面顶部的成功通知(如"系统密码已更改")LuCI 用 info 类插在 + #maincontent 顶部——视觉上被内容淹没。这里搬进主题自建的 + 独立遮罩层 #liquid-notice-overlay 居中弹出,6 秒/点击关闭。 + 不复用 ui.js 的 #modal_overlay:它里面常驻一个预创建的空 + .modal 容器(showModal 底座),混用会把空壳一起显示出来 + (两个框叠着),且 centerModals 会去居中那个空壳。 + 只搬 info/notice/success,danger/warning 留在原位。 */ + var mc = document.getElementById('maincontent'); + if (!mc) + return; + + function getBox() { + var box = document.getElementById('liquid-notice-overlay'); + if (!box) { + box = document.createElement('div'); + box.id = 'liquid-notice-overlay'; + document.body.appendChild(box); + } + return box; + } + + function promote(node) { + if (node.nodeType !== 1 || !node.classList) + return; + if (!node.classList.contains('alert-message')) + return; + if (!(node.classList.contains('info') || + node.classList.contains('notice') || + node.classList.contains('success'))) + return; + if (node.classList.contains('liquid-promoted')) + return; + var box = getBox(); + node.classList.add('liquid-promoted'); + box.appendChild(node); + box.classList.add('show'); + var closed = false; + function close() { + if (closed) + return; + closed = true; + if (node.parentNode) + node.parentNode.removeChild(node); + if (!box.querySelector('.alert-message')) + box.classList.remove('show'); + } + /* 点击任意处(含自带的"关闭"按钮)即关 */ + node.addEventListener('click', close); + setTimeout(close, 6000); + } + + /* 页面加载时已存在的通知 */ + mc.querySelectorAll(':scope > .alert-message').forEach(promote); + if (window.MutationObserver) { + /* SPA 切页/保存后新插入的通知 */ + new MutationObserver(function (muts) { + muts.forEach(function (m) { + m.addedNodes.forEach(promote); + }); + }).observe(mc, { childList: true }); + } + } + + /* ── 页脚版本号 → 检查更新(对齐 pushbot 的检查更新设计)───────── + 访问端浏览器前端 fetch GitHub raw Makefile(8s 超时),与本地 + PKG_VERSION-PKG_RELEASE 比较,四态 frosted toast: + 已最新(绿)/内测版(紫)/检测到更新(橙)/查询失败(红)。 + - 非更新态:8s 自动消失(比 pushbot 多 4s),点卡片超链接作者仓库 + - 更新态:不自动消失;按钮行 = pushbot 同款(一键更新/拉取新包/ + 下载链接/清理包/忽略)+ 多一个"作者仓库"在忽略左边 + - 在线更新走主题自己的 OTA 端点(controller liquid.uc), + 安装完成后倒计时刷新(轮询 act_version,与 pushbot 一致) + 登录页不绑定(保留页脚版本号原跳转仓库超链接)。 */ + function initVersionCheck() { + var REPO = 'https://github.com/zzsj0928/luci-theme-liquid'; + var RAW_MK = 'https://raw.githubusercontent.com/zzsj0928/luci-theme-liquid/main/Makefile'; + + function api(name) { + var b = (window.L && L.env && L.env.admin_path) + ? L.env.admin_path : '/cgi-bin/luci/admin/'; + return b + 'system/liquid/' + name; + } + + /* pushbot 同款版本比较:0.8-r69 → 0.8.69 逐段数字比 */ + function cmpVer(a, b) { + a = String(a || '').replace(/^v/, '').replace(/-r/, '.'); + b = String(b || '').replace(/^v/, '').replace(/-r/, '.'); + var pa = a.split(/[.-]/), pb = b.split(/[.-]/), i, x, y; + for (i = 0; i < Math.max(pa.length, pb.length); i++) { + x = parseInt(pa[i], 10) || 0; + y = parseInt(pb[i], 10) || 0; + if (x > y) return 1; + if (x < y) return -1; + } + return 0; + } + + function ringHtml(pct) { + return '' + + '' + + ''; + } + + /* 在页脚内弹出 frosted 卡片(overlay 锚定页脚盒子 → 页脚宽度内居中)。 + inCard=true 时把提示盖到"检测到更新"卡片内部、卡片内上下左右 + 居中模糊弹出(流程内错误如"下载失败",对齐 pushbot + pb_ota_showError 设计);无更新卡片时退回页脚锚定 */ + function showToast(msg, sub, cls, buttons, autoMs, clickRepo, extraStyle, inCard) { + var badge = document.querySelector('p.luci-foot a.liquid-version-link'); + var footer = badge ? badge.closest('p.luci') : null; + var card = inCard ? document.querySelector('p.luci-foot .liquid-ver-toast.is-update') : null; + var parent = card || footer; + if (!parent) return null; + var ov = document.createElement('div'); + ov.className = 'liquid-ver-overlay' + (card ? ' card-centered' : ''); + var toast = document.createElement('div'); + toast.className = 'liquid-ver-toast ' + cls + (clickRepo ? ' is-clickable' : ''); + if (extraStyle) toast.style.cssText = extraStyle; + var tm = document.createElement('div'); + tm.className = 'liquid-ver-toast-msg'; + tm.textContent = msg; + toast.appendChild(tm); + if (sub) { + var ts = document.createElement('div'); + ts.className = 'liquid-ver-toast-sub'; + ts.textContent = sub; + toast.appendChild(ts); + } + if (buttons && buttons.length > 0) { + var row = document.createElement('div'); + row.className = 'liquid-ota-btn-row'; + buttons.forEach(function(cfg) { + var b = document.createElement('button'); + b.type = 'button'; + b.className = 'liquid-ota-btn'; + b.textContent = cfg.label; + if (cfg.id) b.id = cfg.id; + b.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + if (cfg.onClick) cfg.onClick(b, res); + }); + row.appendChild(b); + }); + toast.appendChild(row); + } + ov.appendChild(toast); + parent.appendChild(ov); + setTimeout(function() { ov.classList.add('show'); }, 20); + var autoId = null; + if (autoMs > 0) { + autoId = setTimeout(function() { res.dismiss(); }, autoMs); + } + var res = { + ov: ov, + dismiss: function() { + if (autoId) { clearTimeout(autoId); autoId = null; } + ov.classList.remove('show'); + setTimeout(function() { ov.remove(); }, 550); + } + }; + /* 非更新态:点卡片超链接作者仓库 */ + if (clickRepo) { + toast.addEventListener('click', function(e) { + if (e.target.closest && e.target.closest('button')) return; + window.open(REPO, '_blank'); + res.dismiss(); + }); + } + return res; + } + + /* 流程内错误提示(下载失败等,pushbot 同款 4s 消失): + 盖在更新卡片上、卡片内居中模糊弹出 */ + function liquidOtaError(msg) { + showToast(msg, null, 'is-err', null, 4000, false, null, true); + } + + /* 安装完成后的倒计时卡片:轮询 act_version,版本变化即刷新, + 10s 兜底强制刷新(与 pushbot pb_ota_showCountdown 一致) */ + function liquidOtaCountdown(local) { + var cdToast = showToast('安装完成', null, 'is-latest', null, 0, false, + 'min-width:150px;max-width:220px;padding:12px 16px;border-radius:10px;text-align:center;transform:scale(0.85);'); + if (!cdToast) return; + cdToast.ov.style.zIndex = '99999'; + cdToast.ov.querySelector('.liquid-ver-toast').innerHTML = + '
    10
    ' + + '
    '; + var cdSec = 10; + var cdTimer = setInterval(function() { + cdSec--; + var el = document.getElementById('liquid_ota_countdown'); + if (el) el.textContent = cdSec; + if (cdSec <= 0) { clearInterval(cdTimer); clearInterval(verTimer); location.reload(); } + }, 1000); + /* 每 2s 轮询主题版本,变了立即刷新 */ + var verTimer = setInterval(function() { + var px = new XMLHttpRequest(); + px.open('GET', api('version') + '?_=' + Date.now()); + px.onload = function() { + try { + var d = JSON.parse(px.responseText); + if (d && d.version && local && d.version !== local) { + clearInterval(verTimer); + clearInterval(cdTimer); + location.reload(); + } + } catch (e) {} + }; + px.send(); + }, 2000); + } + + function bind() { + /* 登录页不接管(保留原跳转仓库超链接),也避免选择器永远 + 匹配不到导致的无限 200ms 重试 */ + if (document.body && document.body.classList.contains('liquid-login')) return; + var badge = document.querySelector('p.luci-foot a.liquid-version-link'); + if (!badge) { setTimeout(bind, 200); return; } + if (badge._lvBound) return; + badge._lvBound = true; + badge.title = '点击检查更新'; + + badge.addEventListener('click', function(e) { + e.preventDefault(); + if (badge.classList.contains('is-checking')) return; + badge.classList.add('is-checking'); + var local = badge.getAttribute('data-ver') || ''; + /* 新检查前清掉旧卡片,避免叠加 */ + var footer = badge.closest('p.luci'); + if (footer) + footer.querySelectorAll('.liquid-ver-overlay').forEach(function(o) { o.remove(); }); + + var ctl = new AbortController(); + var timer = setTimeout(function() { ctl.abort(); }, 8000); + fetch(RAW_MK, { signal: ctl.signal }) + .then(function(r) { + if (!r.ok) throw new Error('HTTP ' + r.status); + return r.text(); + }) + .then(function(txt) { + clearTimeout(timer); + var mv = txt.match(/PKG_VERSION:=([0-9.]+)/); + var mr = txt.match(/PKG_RELEASE:=([0-9]+)/); + var remote = (mv && mr) ? mv[1] + '-r' + mr[1] : ''; + if (!remote) throw new Error('parse'); + var c = cmpVer(local, remote); + var msg, cls; + if (c > 0) { msg = '已是抢先体验的内测版!'; cls = 'is-ahead'; } + else if (c < 0) { msg = '作者仓库发布了新版本!'; cls = 'is-update'; } + else { msg = '已经是最新版本!'; cls = 'is-latest'; } + var sub = '📦 v' + local + ' ☁️ v' + remote; + var remoteVer = mv[1], remoteRel = mr[1]; + + var btns = null; + if (c < 0) { + btns = [ + { + label: '一键更新', + id: 'liquid_ota_oneclick_btn', + onClick: function(btn, r2) { + btn.disabled = true; + btn.innerHTML = ringHtml(0) + '0%'; + /* 与"拉取新包"完全一致的轮询/报错链路(pushbot 原版一键更新缺 + zeroCount 守卫会默默卡 0%):即刻轮询不等 onload、fail→下载失败、 + 卡0%×20s→GitHub Release 无法访问、网络错→下载失败 */ + var zeroCount = 0; + var pollId = setInterval(function() { + var px = new XMLHttpRequest(); + px.open('GET', api('ota_download_progress') + '?_=' + Date.now()); + px.onload = function() { + try { + var pd = JSON.parse(px.responseText); + var pct = pd.progress || '0'; + if (pct === 'done') { + clearInterval(pollId); + btn.textContent = '安装中...'; + var ix = new XMLHttpRequest(); + ix.open('GET', api('ota_install') + '?_=' + Date.now()); + ix.send(); + r2.dismiss(); + liquidOtaCountdown(local); + } else if (pct === 'fail') { + clearInterval(pollId); + btn.disabled = false; + btn.textContent = '一键更新'; + liquidOtaError('下载失败'); + } else { + var num = parseInt(pct, 10) || 0; + if (num === 0) zeroCount++; else zeroCount = 0; + if (zeroCount >= 20) { + clearInterval(pollId); + btn.disabled = false; + btn.textContent = '一键更新'; + liquidOtaError('GitHub Release 无法访问'); + return; + } + btn.innerHTML = ringHtml(num) + num + '%'; + } + } catch (e) {} + }; + px.send(); + }, 1000); + var x = new XMLHttpRequest(); + x.open('GET', api('ota_download') + '?ver=' + encodeURIComponent(remoteVer) + + '&rel=' + encodeURIComponent(remoteRel) + '&_=' + Date.now()); + x.onerror = function() { + clearInterval(pollId); + btn.disabled = false; + btn.textContent = '一键更新'; + liquidOtaError('下载失败'); + }; + x.send(); + } + }, + { + label: '拉取新包', + id: 'liquid_ota_pull_btn', + onClick: function(btn, r2) { + /* 已下载完 → 直接安装 */ + if (btn.getAttribute('data-ready') === '1') { + btn.disabled = true; + btn.textContent = '安装中...'; + var ix = new XMLHttpRequest(); + ix.open('GET', api('ota_install') + '?_=' + Date.now()); + ix.send(); + r2.dismiss(); + liquidOtaCountdown(local); + return; + } + btn.disabled = true; + btn.innerHTML = ringHtml(0) + '0%'; + var zeroCount = 0; + var pollId = setInterval(function() { + var px = new XMLHttpRequest(); + px.open('GET', api('ota_download_progress') + '?_=' + Date.now()); + px.onload = function() { + try { + var pd = JSON.parse(px.responseText); + var pct = pd.progress || '0'; + if (pct === 'done') { + clearInterval(pollId); + btn.disabled = false; + btn.textContent = '安装更新'; + btn.classList.add('liquid-ota-ready'); + btn.setAttribute('data-ready', '1'); + } else if (pct === 'fail') { + clearInterval(pollId); + btn.disabled = false; + btn.textContent = '拉取新包'; + liquidOtaError('下载失败'); + } else { + var num = parseInt(pct, 10) || 0; + if (num === 0) zeroCount++; else zeroCount = 0; + if (zeroCount >= 20) { + clearInterval(pollId); + btn.disabled = false; + btn.textContent = '拉取新包'; + liquidOtaError('GitHub Release 无法访问'); + return; + } + btn.innerHTML = ringHtml(num) + num + '%'; + } + } catch (e) {} + }; + px.send(); + }, 1000); + /* 触发下载(后台进行),网络层失败与一键更新同样立即报错 */ + var xhr = new XMLHttpRequest(); + xhr.open('GET', api('ota_download') + '?ver=' + encodeURIComponent(remoteVer) + + '&rel=' + encodeURIComponent(remoteRel) + '&_=' + Date.now()); + xhr.onerror = function() { + clearInterval(pollId); + btn.disabled = false; + btn.textContent = '拉取新包'; + liquidOtaError('下载失败'); + }; + xhr.send(); + } + }, + { + label: '下载链接', + onClick: function(btn, r2) { + var baseUrl = REPO + '/releases/download/luci-theme-liquid-v' + + remoteVer + '-r' + remoteRel + '/'; + var isApk = false; + var xh = new XMLHttpRequest(); + xh.open('GET', api('detect_pkgmgr') + '?_=' + Date.now(), false); + xh.send(); + if (xh.status === 200) { + try { var d = JSON.parse(xh.responseText); isApk = (d.pkgmgr === 'apk'); } catch (e) {} + } + var file = isApk + ? ('luci-theme-liquid-' + remoteVer + '-r' + remoteRel + '.apk') + : ('luci-theme-liquid_' + remoteVer + '-r' + remoteRel + '_all.ipk'); + window.open(baseUrl + file, '_blank'); + r2.dismiss(); + } + }, + { + label: '清理包', + onClick: function(btn, r2) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', api('ota_clear') + '?_=' + Date.now()); + xhr.onload = function() { + btn.textContent = '已清理'; + btn.disabled = true; + setTimeout(function() { r2.dismiss(); }, 1500); + }; + xhr.send(); + } + }, + { + label: '作者仓库', + onClick: function(btn, r2) { + window.open(REPO, '_blank'); + r2.dismiss(); + } + }, + { + label: '忽略', + onClick: function(btn, r2) { + r2.dismiss(); + } + } + ]; + } + /* 更新态:不自动消失(0);其他态:8s(比 pushbot 多 4s)+ 点卡片跳仓库 */ + showToast(msg, sub, cls, btns, btns ? 0 : 8000, !btns); + }) + .catch(function() { + clearTimeout(timer); + showToast('网络异常,无法访问作者仓库! ☹️', '📦 v' + local, 'is-err', null, 8000, true); + }) + .finally(function() { badge.classList.remove('is-checking'); }); + }); + } + + if (document.readyState === 'complete') + bind(); + else + window.addEventListener('load', bind); + setTimeout(bind, 200); + } + + function portalFixedModals() { + document.querySelectorAll('.version-modal').forEach(function (m) { + if (m.parentNode !== document.body) + document.body.appendChild(m); + }); + } + + /* 文档级滚动配套(r22):LuCI SPA 切页(菜单/面包屑/标签链接 → XHR + 换 #view 内容)不重置文档滚动位置,新页面会停在旧深度;安卓对 + "初始滚动非零"的页面要先把滚到顶再下滑才肯收缩地址栏。这里在 + 点击站内导航链接后监听 #view 直接子节点变化(= 切页渲染完成; + 深层的自动刷新/控件更新不触发,避免误回顶),一旦换页立即把 + 文档滚回顶部——每个页面都从顶开始,首次下滑即可收缩工具栏。 + 整页跳转时观察器随页面销毁,无副作用;5 秒兜底自动撤防 */ + function initNavScrollTop() { + if (typeof MutationObserver == 'undefined') + return; + var mo = null, timer = 0; + function disarm() { + if (timer) { clearTimeout(timer); timer = 0; } + if (mo) { mo.disconnect(); mo = null; } + } + document.addEventListener('click', function (ev) { + if (ev.button !== 0) + return; + var a = (ev.target && ev.target.closest) ? ev.target.closest('a[href]') : null; + if (!a) + return; + var href = a.getAttribute('href') || ''; + /* 只处理站内导航:排除纯锚点、外部链接、javascript 伪协议 */ + if (!href || href.charAt(0) == '#' || + /^(?:[a-z][a-z0-9+.-]*)?\/\//i.test(href) || + href.toLowerCase().indexOf('javascript:') === 0) + return; + var view = document.getElementById('view'); + if (!view) + return; + disarm(); + mo = new MutationObserver(function () { + disarm(); + window.scrollTo(0, 0); + }); + mo.observe(view, { childList: true }); + timer = setTimeout(disarm, 5000); + }, true); + } + + /* 登录页 logo:luci 的 modal.login 是 JS 渲染的,注入内联 SVG + (跟随主题色 + 玻璃水滴感),替换原 CSS 背景图 */ + function injectLoginLogo() { + var m = document.querySelector('body.liquid-login #modal_overlay > .modal.login'); + if (!m || m.querySelector('.liquid-logo')) + return; + var w = document.createElement('div'); + w.innerHTML = ''; + m.insertBefore(w.firstChild, m.firstChild); + } + + /* 窗口尺寸变化时重新计算已打开下拉框的方向 */ + window.addEventListener('resize', function () { + setTimeout(adjustDropdownDirection, 60); + }); + + /* 移动端菜单 top 跟随顶栏实际高度(防顶栏被撑高后菜单盖住它) */ + function syncMenuTop() { + var bar = document.getElementById('menubar'); + var menu = document.getElementById('mainmenu'); + + if (!bar || !menu) + return; + + if (window.matchMedia && window.matchMedia('(max-width: 854px)').matches) + menu.style.top = (bar.offsetHeight + 1) + 'px'; + else + menu.style.top = ''; + } + + /* #indicators 由 ui.js 动态渲染,顶栏高度在 DOMContentLoaded 时可能未定型: + 延迟二次校准 + 汉堡点击时校准 */ + function scheduleMenuTop() { + setTimeout(syncMenuTop, 0); + } + + applyCustomAccent(getAccentCustom()); + guardDarkmode(); + syncThemeColor(); + + if (document.readyState == 'loading') + document.addEventListener('DOMContentLoaded', function () { + flushPending(); + showPendingToast(); + initSwitch(); + initColorSwitch(); + initGlassOpacitySlider(); + syncMenuTop(); + initTabSliders(); + syncDropdownValues(); + fitDropdownWidths(); + portalTooltips(); + portalFixedModals(); + portalTopNotices(); + initVersionCheck(); + fixComboPillClick(); + initNavScrollTop(); + injectLoginLogo(); + setTimeout(syncMenuTop, 300); + setTimeout(initTabSliders, 300); + setTimeout(syncDropdownValues, 300); + }); + else { + flushPending(); + showPendingToast(); + initSwitch(); + initColorSwitch(); + initGlassOpacitySlider(); + syncMenuTop(); + initTabSliders(); + syncDropdownValues(); + fitDropdownWidths(); + portalTooltips(); + portalFixedModals(); + portalTopNotices(); + initVersionCheck(); + fixComboPillClick(); + initNavScrollTop(); + injectLoginLogo(); + setTimeout(syncMenuTop, 300); + setTimeout(initTabSliders, 300); + setTimeout(syncDropdownValues, 300); + } + + /* 定时清除所有内联 opacity(非 0/1 的残留值,luci 的离线/hover + 样式会残留在接口徽章图标/文字、删除按钮、'取消配置'按钮等元素上, + 让已连接内容呈半透、被误判为未连接)。统一压回不透明。 */ + setInterval(function () { + document.querySelectorAll('[style*="opacity"]').forEach(function (el) { + var o = el.style.opacity; + if (o && o !== '1' && o !== '0') + el.style.opacity = ''; + }); + }, 600); + + /* 点击内容区(当前值行)也能稳定展开:LuCI 的 handleClick 虽支持整块 + 点击,但内容区 click 会冒泡到 window 的 closeAllDropdowns,导致 + 打开即关闭(闪烁)。拦截内容区 click,改为以胶囊本身为目标重新 + 触发,走 handleClick 的打开路径(其内部 stopPropagation,不再 + 冒泡到 window)。打开状态下的点击不拦截,LuCI 正常处理关闭。 + 作用于所有表单型 cbi-dropdown(原只匹配 Combobox 试点的 + liquid-combo-pilot class,该替换已移除导致选择器落空); + 排除 .btn/.cbi-button 型(按钮下拉有自己的动作语义) */ + function fixComboPillClick() { + document.querySelectorAll('.cbi-dropdown:not(.btn):not(.cbi-button) > ul > li[display]').forEach(function (li) { + if (li.__liquidPillClick) + return; + li.__liquidPillClick = true; + li.addEventListener('click', function (ev) { + var sb = li.closest('.cbi-dropdown'); + if (!sb || sb.hasAttribute('open')) + return; + ev.stopPropagation(); + ev.preventDefault(); + sb.click(); + }); + }); + } + + /* 页面内容动态变化(view 切换、cbi 渲染等)时初始化新出现的 tab 菜单 */ + if (window.MutationObserver) { + var tabObserver = new MutationObserver(function () { + initTabSliders(); + syncDropdownValues(); + fitDropdownWidths(); + portalTooltips(); + injectLoginLogo(); + fixComboPillClick(); + }); + document.addEventListener('DOMContentLoaded', function () { + tabObserver.observe(document.body, { childList: true, subtree: true }); + }); + } + + document.addEventListener('click', function (e) { + var nav = document.querySelector('#menubar .navigation'); + if (nav && nav.contains(e.target)) { + scheduleMenuTop(); + return; + } + /* 移动端:点击菜单(抽屉)外的任意区域 → 闭合侧边栏。 + 菜单内点击(子菜单展开等)不干预,菜单按钮交给原生 toggle */ + var menu = document.getElementById('mainmenu'); + if (menu && menu.classList.contains('active') && !menu.contains(e.target)) { + menu.classList.remove('active'); + if (nav) + nav.classList.remove('active'); + } + }); + + window.addEventListener('resize', syncMenuTop); + + /* 页面资源加载完成(图标 naturalWidth 就绪)后重测下拉宽度, + 修正带图标选项的宽度估算 */ + window.addEventListener('load', function () { + document.querySelectorAll('.cbi-dropdown.liquid-dd-fit').forEach(function (dd) { + dd.classList.remove('liquid-dd-fit'); + }); + fitDropdownWidths(); + }); + + /* 全局兜底:任何下拉 open 属性变化(含动态 modal 里尚未经 + syncDropdownValues 初始化的 dd)都重算定位 —— 修复某些终端 + 下拉只剩一横条(luci 残留 inline 定位 / maxHeight 1px) */ + if (window.MutationObserver) { + var globalDdObserver = new MutationObserver(function (muts) { + var need = false; + muts.forEach(function (m) { + if (m.type === 'attributes' && m.attributeName === 'open') + need = true; + }); + if (!need) + return; + adjustDropdownDirection(); + /* luci 触屏分支用 rAF 动画(约 100ms)滚动定位,会在微任务 + 之后再次覆盖 inline 样式 —— 延迟再清理一次,覆盖它 + (Windows 触屏/Edge 上尤甚) */ + setTimeout(adjustDropdownDirection, 200); + }); + globalDdObserver.observe(document.documentElement, { + attributes: true, + subtree: true, + attributeFilter: ['open'] + }); + } + + /* ── Overview page memory/storage bars: render used/total text inside + the taller progress bar. LuCI's status include renders +
    + — we inject a centered label from the title attribute and keep it + in sync when LuCI re-renders (poll updates). ── */ + function syncBarLabels() { + document.querySelectorAll('.cbi-section .cbi-progressbar').forEach(function (bar) { + var label = bar.querySelector('span.liquid-bar-label'); + if (!label) { + label = document.createElement('span'); + label.className = 'liquid-bar-label'; + bar.appendChild(label); + } + /* keep in sync with LuCI's poll updates (title attribute) */ + var txt = bar.title || ''; + if (label.textContent !== txt) + label.textContent = txt; + }); + } + + /* overview renders its includes after the view instantiates; poll + both the DOM (async view load) and the title updates */ + if (window.MutationObserver) { + var barObserver = new MutationObserver(function (muts) { + var need = false; + muts.forEach(function (m) { + if (m.type === 'childList' && m.addedNodes && m.addedNodes.length) + need = true; + if (m.type === 'attributes' && m.attributeName === 'title') + need = true; + }); + if (need) + syncBarLabels(); + }); + barObserver.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['title'] + }); + } + setTimeout(syncBarLabels, 500); + setTimeout(syncBarLabels, 1500); + + /* ── 矮弹窗居中器(限定范围:仅应用提示/待应用等矮弹窗)── + 安卓浏览器布局视口(innerHeight) 含地址栏区域,与真实可视区 + 不一致(如 811 vs 695),且地址栏展开/收起时动态变化,纯 CSS + 无法感知 —— 这是 visualViewport API 存在的原因。 + 用法:仅当弹窗内容高度 < 可视区(矮弹窗,居中是刚需)时用 + visualViewport 像素级居中;超高弹窗(表单类如添加 DHCP) + 绝不锁定,保持文档流 + overlay 滚动,不影响桌面端功能。 */ + function centerModals() { + var vv = window.visualViewport; + var overlay = document.getElementById('modal_overlay'); + var modal = overlay ? overlay.firstElementChild : null; + if (!modal || document.body.classList.contains('liquid-login')) + return; + var vh = vv ? vv.height : window.innerHeight; + var mh = modal.offsetHeight || 0; + if (!mh) + return; + /* 超高弹窗:不锁定,恢复文档流(margin 顶部对齐 + overlay 滚动) */ + if (mh >= vh - 24) { + modal.style.position = ''; + modal.style.top = ''; + modal.style.left = ''; + modal.style.margin = ''; + modal.style.transform = ''; + return; + } + /* 矮弹窗:visualViewport 像素级精确居中 */ + var vw = vv ? vv.width : window.innerWidth; + var vx = vv ? vv.offsetLeft : 0; + var vy = vv ? vv.offsetTop : 0; + var mw = modal.offsetWidth || 0; + modal.style.position = 'fixed'; + modal.style.top = Math.round(vy + (vh - mh) / 2) + 'px'; + modal.style.left = Math.round(vx + (vw - mw) / 2) + 'px'; + modal.style.margin = '0'; + modal.style.transform = 'none'; + } + + /* 监听弹窗出现/内容变化(应用提示每秒重建;表单弹窗内容逐步渲染) */ + var modalTimer = null; + function watchModals() { + if (document.body.classList.contains('modal-overlay-active')) { + if (!modalTimer) { + centerModals(); + modalTimer = setInterval(centerModals, 200); + } + } else if (modalTimer) { + clearInterval(modalTimer); + modalTimer = null; + } + } + new MutationObserver(watchModals).observe(document.body, { + attributes: true, + attributeFilter: ['class'] + }); + window.addEventListener('resize', centerModals); + if (window.visualViewport) + window.visualViewport.addEventListener('resize', centerModals); + + /* ── 多实例 section 子卡片(Dropbear 实例等)────────── + 同一 .cbi-section 内有多个平级 .cbi-section-node 时,给每个 + 节点加 liquid-multi-node(CSS 渲染为独立子卡片+间隔), + 同时给外层 section 加 liquid-multi-sec(删除按钮收边等)。 + MutationObserver 覆盖动态增删实例(点击"添加实例")。 */ + function styleMultiSections() { + document.querySelectorAll('.cbi-section').forEach(function (sec) { + var nodes = sec.querySelectorAll(':scope > .cbi-section-node'); + if (nodes.length > 1) { + nodes.forEach(function (n) { + n.classList.add('liquid-multi-node'); + }); + sec.classList.add('liquid-multi-sec'); + } + else { + sec.classList.remove('liquid-multi-sec'); + } + }); + } + styleMultiSections(); + if (window.MutationObserver) { + var msObs = new MutationObserver(function () { + styleMultiSections(); + }); + msObs.observe(document.body, { childList: true, subtree: true }); + } + + /* ── 宽表格:优先折叠按钮,再折叠文字 ────────────────────── + 判定量的是“所有列按自身内容排开所需的宽度”(max-content:按钮 + 横排一行、文字不换行时的宽度),而不是表格实际渲染宽度——auto + 表格布局在列内容总宽超出表宽时会先压文字列、表格盒本身仍装得下, + 量实际宽度会漏判,结果就是文字先于按钮被折叠。 + + 只要该需求宽度放不下(超出所在容器的内容宽或屏幕宽),就给表格 + 加 liquid-actions-overflow:操作列放开 form.js + stabilizeActionColumnWidth 写死的内联列宽、按钮改为每个独占一行, + 宽度先让给文字;文字拿回后仍不够,才由文字换行兜底(对应 CSS 见 + cascade.css 末尾段落)。 + + 判定始终先移除类再量(量到的才是“按钮横排”的需求),避免加类→ + 不溢出→去类→又溢出的震荡;只处理含操作列的表格,无按钮的表格 + 维持原有排版与横向滚动。 */ + var TBL_CLS = 'liquid-actions-overflow'; + var tblTimer = null; + + /* 元素内容盒宽度:clientWidth 减掉左右 padding(表格比的是父级内容区) */ + function contentBoxWidth(el) { + var cs = window.getComputedStyle(el); + return el.clientWidth - + (parseFloat(cs.paddingLeft) || 0) - + (parseFloat(cs.paddingRight) || 0); + } + + /* 临时按内容展开量一次表格的需求宽(列不换行、按钮横排一行), + 同步读取后立即还原样式,同一帧内不会产生可见闪烁 */ + function needWidth(t) { + var st = t.style; + var w0 = st.width, mw0 = st.minWidth, mx0 = st.maxWidth; + st.width = 'max-content'; + st.minWidth = '0'; + st.maxWidth = 'none'; + var w = t.getBoundingClientRect().width; + st.width = w0; + st.minWidth = mw0; + st.maxWidth = mx0; + return w; + } + + function tableOverflows(t) { + var doc = document.documentElement; + var parent = t.parentElement; + /* 1) 表格内容已超出表格盒(自身裁剪 / 内部横滚) */ + if (t.scrollWidth > t.clientWidth + 1) + return true; + /* 2) 核心:需求宽(按钮横排放得下)> 可用宽 → 从按钮开始折 */ + var need = needWidth(t); + var avail = parent ? contentBoxWidth(parent) : Infinity; + if (need > avail + 1 || need > doc.clientWidth + 1) + return true; + /* 3) 表格盒右缘已越出屏幕、且文档无法横向滚过去(被祖先裁切) */ + var rect = t.getBoundingClientRect(); + return rect.right > doc.clientWidth + 1 && doc.scrollWidth <= doc.clientWidth + 1; + } + + function updateOverflowTables() { + var list = document.querySelectorAll('table.cbi-section-table, table.table'); + for (var i = 0; i < list.length; i++) { + var t = list[i]; + t.classList.remove(TBL_CLS); + /* 清掉 form.js stabilizeActionColumnWidth 写死的内联列宽: + 类还在时它量到的是“按钮纵排”的宽度(最宽按钮 ~63px), + 窗口变宽、类移除后这个残留值会让 needWidth 少算一截、 + 判定漏折;清掉后列宽回归 auto 布局(= 按钮横排的 + max-content,与 stabilize 结果一致),量到的才是 + “按钮横排放得下”所需的真实宽度 */ + var acts = t.querySelectorAll('th.cbi-section-actions, td.cbi-section-actions'); + for (var j = 0; j < acts.length; j++) { + if (acts[j].style.width || acts[j].style.minWidth) { + acts[j].style.width = ''; + acts[j].style.minWidth = ''; + } + } + if (t.querySelector('.cbi-section-actions') && tableOverflows(t)) + t.classList.add(TBL_CLS); + } + } + + function scheduleOverflowTables() { + if (tblTimer) + clearTimeout(tblTimer); + /* 延后 120ms:排在 form.js 的 setTimeout(stabilize 列宽) 与其 + resize 处理之后量,拿到的才是内联列宽写完后的稳定状态 */ + tblTimer = setTimeout(function () { + tblTimer = null; + updateOverflowTables(); + }, 120); + } + + scheduleOverflowTables(); + window.addEventListener('load', scheduleOverflowTables); + window.addEventListener('resize', scheduleOverflowTables); + if (window.MutationObserver) { + /* 行增删、轮询刷新状态文本都会改变表格宽度 */ + new MutationObserver(scheduleOverflowTables) + .observe(document.body, { childList: true, subtree: true }); + } +})(); diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/svg/auto.svg b/luci-theme-liquid/htdocs/luci-static/liquid/svg/auto.svg new file mode 100644 index 000000000..a686e6037 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/svg/auto.svg @@ -0,0 +1 @@ + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/svg/chevron-down.svg b/luci-theme-liquid/htdocs/luci-static/liquid/svg/chevron-down.svg new file mode 100644 index 000000000..c74a1e934 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/svg/chevron-down.svg @@ -0,0 +1 @@ + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/svg/chevron-right.svg b/luci-theme-liquid/htdocs/luci-static/liquid/svg/chevron-right.svg new file mode 100644 index 000000000..fb66d958f --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/svg/chevron-right.svg @@ -0,0 +1 @@ + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/svg/close.svg b/luci-theme-liquid/htdocs/luci-static/liquid/svg/close.svg new file mode 100644 index 000000000..8a9b5d302 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/svg/close.svg @@ -0,0 +1 @@ + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/svg/devices.svg b/luci-theme-liquid/htdocs/luci-static/liquid/svg/devices.svg new file mode 100644 index 000000000..2ab09176b --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/svg/devices.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/svg/internet.svg b/luci-theme-liquid/htdocs/luci-static/liquid/svg/internet.svg new file mode 100644 index 000000000..8563603c9 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/svg/internet.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/svg/lock.svg b/luci-theme-liquid/htdocs/luci-static/liquid/svg/lock.svg new file mode 100644 index 000000000..7904640b5 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/svg/lock.svg @@ -0,0 +1 @@ + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/svg/logo.svg b/luci-theme-liquid/htdocs/luci-static/liquid/svg/logo.svg new file mode 100644 index 000000000..90df19fd7 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/svg/logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/svg/moon.svg b/luci-theme-liquid/htdocs/luci-static/liquid/svg/moon.svg new file mode 100644 index 000000000..08cc01d93 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/svg/moon.svg @@ -0,0 +1 @@ + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/svg/not-internet.svg b/luci-theme-liquid/htdocs/luci-static/liquid/svg/not-internet.svg new file mode 100644 index 000000000..d66f8379f --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/svg/not-internet.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/svg/refresh.svg b/luci-theme-liquid/htdocs/luci-static/liquid/svg/refresh.svg new file mode 100644 index 000000000..411eb0def --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/svg/refresh.svg @@ -0,0 +1 @@ + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/svg/router.svg b/luci-theme-liquid/htdocs/luci-static/liquid/svg/router.svg new file mode 100644 index 000000000..1ff29ee56 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/svg/router.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/svg/search.svg b/luci-theme-liquid/htdocs/luci-static/liquid/svg/search.svg new file mode 100644 index 000000000..b6c3455cb --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/svg/search.svg @@ -0,0 +1 @@ + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/svg/spinner.svg b/luci-theme-liquid/htdocs/luci-static/liquid/svg/spinner.svg new file mode 100644 index 000000000..271a25a44 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/svg/spinner.svg @@ -0,0 +1 @@ + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/svg/sun.svg b/luci-theme-liquid/htdocs/luci-static/liquid/svg/sun.svg new file mode 100644 index 000000000..a28169e8c --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/svg/sun.svg @@ -0,0 +1 @@ + diff --git a/luci-theme-liquid/htdocs/luci-static/liquid/svg/wireless.svg b/luci-theme-liquid/htdocs/luci-static/liquid/svg/wireless.svg new file mode 100644 index 000000000..576baafe8 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/liquid/svg/wireless.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-theme-liquid/htdocs/luci-static/resources/menu-liquid.js b/luci-theme-liquid/htdocs/luci-static/resources/menu-liquid.js new file mode 100644 index 000000000..b61ecbf99 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/resources/menu-liquid.js @@ -0,0 +1,296 @@ +'use strict'; +'require baseclass'; +'require ui'; + +/* Liquid theme menu renderer, based on luci-theme-openwrt-2020's + * menu-openwrt2020.js (Apache 2.0). Renders the left sidebar into + * #mainmenu, the mode menu into #modemenu and sub page tabs into + * #tabmenu. */ + +return baseclass.extend({ + __init__() { + ui.menu.load().then((tree) => this.render(tree)); + }, + + render(tree) { + let node = tree; + let url = ''; + + this.renderModeMenu(node); + + if (L.env.dispatchpath.length >= 3) { + for (var i = 0; i < 3 && node; i++) { + node = node.children[L.env.dispatchpath[i]]; + url = url + (url ? '/' : '') + L.env.dispatchpath[i]; + } + + if (node) + this.renderTabMenu(node, url); + } + + document.querySelector('#menubar > .navigation') + .addEventListener('click', ui.createHandlerFn(this, 'handleSidebarToggle')); + + this.initMenuIndicator(); + }, + + /* hover 追踪:一个玻璃滑块平滑跟随鼠标在一级菜单项间移动 */ + initMenuIndicator() { + const menu = document.querySelector('#mainmenu'); + + if (!menu) + return; + + const indicator = E('div', { 'class': 'liquid-menu-indicator' }); + menu.appendChild(indicator); + + const items = menu.querySelectorAll('ul.mainmenu.l1 > li'); + + /* 追踪鼠标位置,供 handleMenuExpand 展开/折叠后重新定位滑块 */ + var self = this; + self._menu = menu; + self._indicator = indicator; + self._lastX = 0; + self._lastY = 0; + + menu.addEventListener('mousemove', function (ev) { + self._lastX = ev.clientX; + self._lastY = ev.clientY; + }); + + menu.addEventListener('mouseleave', function () { + menu.classList.remove('liquid-menu-hovering'); + }); + + menu.addEventListener('scroll', function () { + menu.classList.remove('liquid-menu-hovering'); + menu.querySelectorAll('ul.mainmenu.l2.liquid-sub-hovering').forEach(function (ul) { + ul.classList.remove('liquid-sub-hovering'); + }); + }); + + items.forEach(function (li) { + li.addEventListener('mouseenter', function () { + if (li.classList.contains('selected')) { + /* 当前菜单已有自身高亮,滑块滑到它时不叠加,避免遮挡 */ + menu.classList.remove('liquid-menu-hovering'); + return; + } + + const a = li.querySelector(':scope > a'); + const menuRect = menu.getBoundingClientRect(); + const aRect = a.getBoundingClientRect(); + + indicator.style.transform = + 'translateY(%dpx)'.format(aRect.top - menuRect.top + menu.scrollTop); + indicator.style.height = '%dpx'.format(aRect.height); + menu.classList.add('liquid-menu-hovering'); + }); + }); + + /* 二级菜单追踪滑块:每个展开的 ul.l2 一个 */ + menu.querySelectorAll('ul.mainmenu.l2').forEach(function (ul) { + const sub = E('div', { 'class': 'liquid-sub-indicator' }); + ul.appendChild(sub); + + ul.querySelectorAll(':scope > li').forEach(function (li) { + li.addEventListener('mouseenter', function () { + if (li.classList.contains('selected')) { + /* 当前选中项已有胶囊高亮,滑块不叠加 */ + ul.classList.remove('liquid-sub-hovering'); + return; + } + + const a = li.querySelector(':scope > a'); + const ulRect = ul.getBoundingClientRect(); + const aRect = a.getBoundingClientRect(); + + sub.style.transform = 'translateY(%dpx)'.format(aRect.top - ulRect.top); + sub.style.height = '%dpx'.format(aRect.height); + ul.classList.add('liquid-sub-hovering'); + }); + }); + + ul.addEventListener('mouseleave', function () { + ul.classList.remove('liquid-sub-hovering'); + }); + }); + }, + + /* 展开/折叠后,用当前鼠标位置重新定位一级滑块。 + 点击切换分类时菜单项垂直位移,但鼠标未离开——mouseenter + 不触发,滑块停留在旧位置。此处用最近的鼠标坐标查出 + 当前悬停的 li 并重算 translateY。 */ + _repositionMenuIndicator() { + var menu = this._menu; + var indicator = this._indicator; + if (!menu || !indicator || !menu.classList.contains('liquid-menu-hovering')) + return; + + var hovered = document.elementFromPoint(this._lastX, this._lastY); + if (!hovered) + return; + + var li = hovered.closest('ul.mainmenu.l1 > li'); + if (!li || li.classList.contains('selected')) { + menu.classList.remove('liquid-menu-hovering'); + return; + } + + var a = li.querySelector(':scope > a'); + if (!a) + return; + + var menuRect = menu.getBoundingClientRect(); + var aRect = a.getBoundingClientRect(); + indicator.style.transform = + 'translateY(%dpx)'.format(aRect.top - menuRect.top + menu.scrollTop); + indicator.style.height = '%dpx'.format(aRect.height); + }, + + handleMenuExpand(ev) { + const a = ev.target; + const li = a.parentNode; + const ul1 = li.parentNode; + const ul2 = a.nextElementSibling; + + /* 点击已展开的一级菜单 → 折叠(收起其子菜单) */ + if (li.classList.contains('active') && ul2 && ul2.children.length > 0) { + li.classList.remove('active'); + a.blur(); + this._repositionMenuIndicator(); + ev.preventDefault(); + ev.stopPropagation(); + return; + } + + /* 未展开:收起其他已展开的一级菜单,再展开当前 */ + document.querySelectorAll('ul.mainmenu.l1 > li.active').forEach(other => { + if (other !== li) + other.classList.remove('active'); + }); + + if (!ul2) + return; + + if (ul2.parentNode.offsetLeft + ul2.offsetWidth <= ul1.offsetLeft + ul1.offsetWidth) + ul2.classList.add('align-left'); + + ul1.classList.add('active'); + li.classList.add('active'); + a.blur(); + + /* 展开/折叠后菜单项垂直位移,滑块位置需跟着更新 */ + this._repositionMenuIndicator(); + + ev.preventDefault(); + ev.stopPropagation(); + }, + + renderMainMenu(tree, url, level) { + const l = (level || 0) + 1; + const ul = E('ul', { 'class': 'mainmenu l%d'.format(l) }); + const children = ui.menu.getChildren(tree); + + if (children.length == 0 || l > 2) + return E([]); + + children.forEach(child => { + /* 当前所在一级菜单:高亮(selected)并展开其二级菜单(active); + 其余一级菜单保持收起,点击才展开 */ + const isActive = (L.env.dispatchpath[l] == child.name); + const activeClass = 'mainmenu-item-%s%s'.format( + child.name, isActive ? ' selected active' : ''); + + ul.appendChild(E('li', { 'class': activeClass }, [ + E('a', { + 'href': L.url(url, child.name), + 'data-title': child.title, + 'click': (l == 1) ? ui.createHandlerFn(this, 'handleMenuExpand') : '' + }, [ + _(child.title) + ]), + this.renderMainMenu(child, url + '/' + child.name, l) + ])); + }); + + if (l == 1) + document.querySelector('#mainmenu').appendChild(E('div', [ ul ])); + + return ul; + }, + + renderModeMenu(tree) { + const menu = document.querySelector('#modemenu'); + const children = ui.menu.getChildren(tree); + + children.forEach((child, index) => { + const isActive = L.env.requestpath.length + ? child.name === L.env.requestpath[0] + : index === 0; + + if (index > 0) + menu.appendChild(E([], ['\u00a0|\u00a0'])); + + menu.appendChild(E('div', { 'class': isActive ? 'active' : '' }, [ + E('a', { href: L.url(child.name) }, [ + _(child.title) + ]) + ])); + + if (isActive) + this.renderMainMenu(child, child.name); + }); + + if (menu.children.length > 1) + menu.style.display = ''; + }, + + renderTabMenu(tree, url, level) { + const container = document.querySelector('#tabmenu'); + const l = (level || 0) + 1; + const ul = E('ul', { 'class': 'cbi-tabmenu' }); + const children = ui.menu.getChildren(tree); + let activeNode = null; + + if (children.length == 0) + return E([]); + + children.forEach(child => { + const isActive = (L.env.dispatchpath[l + 2] == child.name); + const activeClass = isActive ? ' cbi-tab' : ''; + const className = 'tabmenu-item-%s %s'.format(child.name, activeClass); + + ul.appendChild(E('li', { 'class': className }, [ + E('a', { 'href': L.url(url, child.name) }, [ + _(child.title) + ]) + ])); + + if (isActive) + activeNode = child; + }); + + container.appendChild(ul); + container.style.display = ''; + + if (activeNode) + container.appendChild(this.renderTabMenu(activeNode, url + '/' + activeNode.name, l)); + + return ul; + }, + + handleSidebarToggle(ev) { + const btn = ev.currentTarget; + const bar = document.querySelector('#mainmenu'); + + if (btn.classList.contains('active')) { + btn.classList.remove('active'); + bar.classList.remove('active'); + } + else { + btn.classList.add('active'); + bar.classList.add('active'); + } + } +}); diff --git a/luci-theme-liquid/htdocs/luci-static/resources/view/liquid/sysauth.js b/luci-theme-liquid/htdocs/luci-static/resources/view/liquid/sysauth.js new file mode 100644 index 000000000..d4cc7d833 --- /dev/null +++ b/luci-theme-liquid/htdocs/luci-static/resources/view/liquid/sysauth.js @@ -0,0 +1,45 @@ +'use strict'; +'require ui'; +'require view'; + +/* Liquid theme login — fully static DOM. The form is rendered directly in +
    +
    +{% endif %} + +{% if (blank_page): %} +

    + Liquid Theme {{ theme_ver }} + · + {{ fw_text }} + · + Kernel: {{ boardinfo.kernel ?? '?' }} + · + {{ version.luciname }} {{ version.luciversion }} +

    +{% endif %} +{% if (!blank_page): %} +

    + Liquid Theme {{ theme_ver }} + · + {{ fw_text }} + · + Kernel: {{ boardinfo.kernel ?? '?' }} + · + {{ version.luciname }} {{ version.luciversion }} +

    +{% endif %} + +{% if (!blank_page): %} + +{% endif %} + + + + diff --git a/luci-theme-liquid/ucode/template/themes/liquid/header.ut b/luci-theme-liquid/ucode/template/themes/liquid/header.ut new file mode 100644 index 000000000..3ed12a2a0 --- /dev/null +++ b/luci-theme-liquid/ucode/template/themes/liquid/header.ut @@ -0,0 +1,252 @@ +{# + Copyright 2026 然后七年 <69092025+zzsj0928@users.noreply.github.com> + Licensed to the public under the Apache License 2.0. +-#} + +{% + import * as fs from 'fs'; + import { popen } from 'fs'; + import { getuid, getspnam } from 'luci.core'; + + const boardinfo = ubus.call('system', 'board') ?? {}; + + /* 从包数据库读取版本号(支持 apk 和 opkg)。 + 优化:mtime 校验 + /tmp 缓存,避免每次渲染 fork awk。 + 包安装/卸载/升级会更新 db mtime → 缓存自动失效重建,版本零滞后。 */ + let theme_ver = ''; + let db_file = '/lib/apk/db/installed'; + let db_mtime = 0; + let st = fs.stat(db_file); + if (st && st.type == 'file') { + db_mtime = st.mtime; + } else { + db_file = '/usr/lib/opkg/status'; + st = fs.stat(db_file); + if (st && st.type == 'file') + db_mtime = st.mtime; + } + if (db_mtime > 0) { + let cm = match(fs.readfile('/tmp/liquid-version.cache') ?? '', /^(\d+) (.*)$/); + if (cm !== null && sprintf('%d', db_mtime) == cm[1]) + theme_ver = cm[2]; + } + if (theme_ver == '') { + let f = popen("awk '/^P:luci-theme-liquid$/{f=1;next} f&&/^V:/{print substr($0,3);exit}' " + db_file + " 2>/dev/null", "r"); + if (f) { theme_ver = replace(f.read("all"), /\s+/, ""); f.close(); } + if (theme_ver == '') { + f = popen("awk '/^Package: luci-theme-liquid$/{f=1;next} f&&/^Version:/{print $2;exit}' /usr/lib/opkg/status 2>/dev/null", "r"); + if (f) { theme_ver = replace(f.read("all"), /\s+/, ""); f.close(); } + } + if (theme_ver != '' && db_mtime > 0) + fs.writefile('/tmp/liquid-version.cache', sprintf('%d %s', db_mtime, theme_ver)); + } + if (theme_ver == '') theme_ver = '?'; + + http.prepare_content('text/html; charset=UTF-8'); + + let ptitle = (node ?? dispatched)?.title; + + /* ---- 主题配置(uci /etc/config/liquid,换客户端保留) ---- */ + let cfg_mode = 'auto'; + let cfg_accent = 'blue'; + let cfg_accent_custom = ''; + let cfg_bing = '0'; + let cfg_glass_opacity = ''; + let cfg = fs.readfile('/etc/config/liquid') ?? ''; + let cm = match(cfg, /option[ \t]+mode[ \t]+'?([^' \t\n]+)'?/); + if (cm !== null) + cfg_mode = cm[1]; + let ca = match(cfg, /option[ \t]+accent[ \t]+'?([^' \t\n]+)'?/); + if (ca !== null) + cfg_accent = ca[1]; + let cc = match(cfg, /option[ \t]+accent_custom[ \t]+'?([^' \t\n]+)'?/); + if (cc !== null) + cfg_accent_custom = cc[1]; + let cb = match(cfg, /option[ \t]+bing[ \t]+'?([^' \t\n]+)'?/); + if (cb !== null) + cfg_bing = cb[1]; + let co = match(cfg, /option[ \t]+glass_opacity[ \t]+'?([^' \t\n]+)'?/); + if (co !== null) + cfg_glass_opacity = co[1]; + + /* ---- Bing 每日壁纸:缓存到各设备自己的 /tmp/liquid/, + 软链 /www/liquid-cache 供 http 访问(/www 为 overlay 可写)。 + system() 只透传 stdout(返回退出码),因此用"写临时文件 + + fs.readfile"获取 wget 输出 ---- */ + let bing_style = ''; + let bing_ok = '0'; + let accent_custom_style = ''; + let glass_opacity_style = ''; + if (cfg_accent == 'custom' && cfg_accent_custom != '') + accent_custom_style = sprintf(':root{--accent-custom:%s;}', cfg_accent_custom); + if (cfg_glass_opacity != '') { + let op = int(cfg_glass_opacity, 10); + if (op < 0) op = 0; + if (op > 100) op = 100; + /* 亮/暗参考 alpha 不同,JS 会根据 data-darkmode 动态修正, + 此处只注入 slider 值供 JS 首帧读取 */ + let def_light = 42; + let def_dark = 40; + let f_light = def_light > 0 ? sprintf('%.2f', op / def_light) : '1.00'; + let f_dark = def_dark > 0 ? sprintf('%.2f', op / def_dark) : '1.00'; + glass_opacity_style = sprintf(':root[data-darkmode="false"]{--glass-opacity:%s;}:root[data-darkmode="true"]{--glass-opacity:%s;}', f_light, f_dark); + } + + if (cfg_bing == '1') { + system('mkdir -p /tmp/liquid && ln -sfn /tmp/liquid /www/liquid-cache && date +%Y%m%d > /tmp/liquid-date.txt'); + let today = trim(fs.readfile('/tmp/liquid-date.txt') ?? ''); + if (today != '') { + let bing_file = sprintf('/tmp/liquid/bing-%s.jpg', today); + if (fs.stat(bing_file)) { + bing_style = sprintf(":root{--liquid-bing:url('/liquid-cache/bing-%s.jpg');}", today); + bing_ok = '1'; + /* 无论今天壁纸是新下载还是已缓存,只要确认存在, + 就清理 /tmp/liquid/ 下所有历史壁纸,只保留今天的 */ + system(sprintf('ls /tmp/liquid/bing-*.jpg 2>/dev/null | grep -v "bing-%s.jpg" | xargs rm -f 2>/dev/null', today)); + } else { + system('wget -qO /tmp/liquid-bing-api.json -T 8 "https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=zh-CN"'); + let api = fs.readfile('/tmp/liquid-bing-api.json') ?? ''; + if (api != '') { + let j = json(api); + if (j && j.images && j.images[0] && j.images[0].urlbase) { + let u = sprintf('https://www.bing.com%s_1920x1080.jpg', j.images[0].urlbase); + system(sprintf('wget -qO "%s" -T 20 "%s"', bing_file, u)); + if (fs.stat(bing_file)) { + bing_style = sprintf(":root{--liquid-bing:url('/liquid-cache/bing-%s.jpg');}", today); + bing_ok = '1'; + /* 新壁纸下载成功后清理 /tmp/liquid/ 下所有旧壁纸, + 只保留今天的文件,防止壁纸随时间堆积占满 /tmp */ + system(sprintf('ls /tmp/liquid/bing-*.jpg 2>/dev/null | grep -v "bing-%s.jpg" | xargs rm -f 2>/dev/null', today)); + } + } + } + } + } + } +-%} + + + + + + + + + + +{% if (glass_opacity_style != ''): %}{% endif %} +{% if (bing_style != ''): %}{% endif %} +{% if (accent_custom_style != ''): %}{% endif %} + + + + + + +{{ striptags(`${boardinfo.hostname ?? '?'}${ptitle ? ` | ${_(ptitle)}` : ''}`) }} +{% if (css): %} + +{% endif %} + + + + + +{% if (!blank_page): %} + + + + +
    + + +
    + {% if (getuid() == 0 && getspnam('root')?.pwdp === '' && join('-', ctx.request_path) != 'admin-system-admin'): %} +
    +

    {{ _('No password set!') }}

    +

    {{ _('There is no password set on this router. Please configure a root password to protect the web interface.') }}

    + {% if (dispatcher.lookup("admin/system/admin")): %} + + {% endif %} +
    + {% endif %} + + {% if (boardinfo.rootfs_type == "initramfs"): %} +
    +

    {{ _('System running in recovery (initramfs) mode.') }}

    +

    {{ _('No changes to settings will be stored and are lost after rebooting. This mode should only be used to install a firmware upgrade') }}

    + {% if (dispatcher.lookup("admin/system/flash")): %} + + {% endif %} +
    + {% endif %} + + +{% endif %} diff --git a/luci-theme-liquid/ucode/template/themes/liquid/sysauth.ut b/luci-theme-liquid/ucode/template/themes/liquid/sysauth.ut new file mode 100644 index 000000000..ae9bc74aa --- /dev/null +++ b/luci-theme-liquid/ucode/template/themes/liquid/sysauth.ut @@ -0,0 +1,122 @@ +{# + Copyright 2026 然后七年 <69092025+zzsj0928@users.noreply.github.com> + Licensed to the public under the Apache License 2.0. +-#} + +{% include('header', { blank_page: true }) %} + +{# 完全静态登录页:form 直接渲染在 DOM 中,无 hidden/opacity、无 DOM 移动、 + 不依赖 LuCI 的 #modal_overlay/ui.showModal。CSS (#liquid-login) 负责 + 全屏遮罩 + 居中玻璃卡片。密码管理器(Bitwarden 等)任何时机扫描 + 都能看到完整字段,无竞态。 #} + +
    + +
    + +
    +
    {{ _('Loading view…') }}
    + +
    + +{% include('footer', { blank_page: true }) %}