frontend actualizado y mejorado extremadamente
1
frontend/dist/assets/index-D-z4-e8u.css
vendored
@@ -1 +0,0 @@
|
||||
html,body{margin:0;padding:0}table th,table td{padding:4px 6px;border-bottom:1px solid #eee}button{padding:6px 10px;cursor:pointer}input{padding:6px 8px}
|
||||
40
frontend/dist/assets/index-D2UhTBFe.js
vendored
Normal file
1
frontend/dist/assets/index-D8GGMeXj.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
:root{--bg: 15 15 18;--fg: 235 235 240;--muted: 180 180 190;--accent: 80 160 255;--card: 28 28 34 / .55;--border: 255 255 255 / .12;--glass-blur: 14px;--radius: 14px}:root.light{--bg: 245 245 248;--fg: 20 20 22;--muted: 110 110 120;--accent: 18 108 242;--card: 255 255 255 / .6;--border: 0 0 0 / .08}*{box-sizing:border-box}html,body,#app{height:100%}html,body{margin:0;padding:0;background:rgb(var(--bg));color:rgb(var(--fg))}body{font:14px/1.45 system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,Helvetica,Arial,"Apple Color Emoji","Segoe UI Emoji"}button{cursor:pointer}a{color:inherit}.topbar{position:sticky;top:0;z-index:10;display:flex;flex-wrap:wrap;align-items:center;gap:10px;padding:10px 14px;-webkit-backdrop-filter:blur(var(--glass-blur));backdrop-filter:blur(var(--glass-blur));background:linear-gradient(180deg,rgba(var(--card)),rgba(var(--card)) 60%,#0000);border-bottom:1px solid rgba(var(--border))}.title{font-size:16px;font-weight:700;letter-spacing:.2px;flex:1 1 auto}.actions{display:inline-flex;flex-wrap:wrap;gap:8px;align-items:center}.icon-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:8px 10px;border-radius:10px;border:1px solid rgba(var(--border));background:rgba(var(--card));color:inherit;transition:transform .12s ease,background .2s;-webkit-backdrop-filter:blur(var(--glass-blur));backdrop-filter:blur(var(--glass-blur))}.icon-btn:hover{transform:translateY(-1px);background:rgba(var(--card))}.icon{width:16px;height:16px;opacity:.9}.shell{height:calc(100vh - 54px);display:grid;grid-template-columns:360px 1fr;gap:12px;padding:12px}.panel{border:1px solid rgba(var(--border));background:rgba(var(--card));border-radius:var(--radius);-webkit-backdrop-filter:blur(var(--glass-blur));backdrop-filter:blur(var(--glass-blur));overflow:hidden;display:flex;flex-direction:column;min-height:0}.panel-header{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid rgba(var(--border))}.panel-title{font-weight:600}.panel-actions{display:inline-flex;flex-wrap:wrap;gap:6px}.scroll{overflow:auto;padding:10px}.card{border:1px solid rgba(var(--border));background:rgba(var(--card));border-radius:12px;padding:10px;transition:transform .12s ease,box-shadow .2s ease;box-shadow:0 4px 14px #00000014}.card:hover{transform:translateY(-1px);box-shadow:0 8px 20px #0000001f}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:10px}@media (max-width: 980px){.shell{grid-template-columns:1fr}}.panel.collapsed .scroll{display:none}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000059;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);display:grid;place-items:center;z-index:20;animation:fadeIn .15s ease}.modal{width:min(680px,92vw);border-radius:14px;border:1px solid rgba(var(--border));background:rgba(var(--card));padding:14px;box-shadow:0 10px 32px #0003}.modal-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}.modal-footer{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.chip{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;border:1px solid rgba(var(--border));border-radius:999px;background:rgba(var(--card));font-size:12px}.muted{color:rgb(var(--muted))}.row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.spacer{flex:1}.toggle{padding:6px 10px;border-radius:8px;border:1px solid rgba(var(--border));background:rgba(var(--card))}
|
||||
17
frontend/dist/assets/index-j5D-XEQH.js
vendored
1
frontend/dist/icons/clear.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6 19a2 2 0 002 2h8a2 2 0 002-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
||||
|
After Width: | Height: | Size: 171 B |
1
frontend/dist/icons/copy.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
|
||||
|
After Width: | Height: | Size: 226 B |
1
frontend/dist/icons/download.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5 20h14v-2H5v2zM11 3h2v8h3l-4 4-4-4h3V3z"/></svg>
|
||||
|
After Width: | Height: | Size: 140 B |
1
frontend/dist/icons/filter.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M3 5h18l-7 8v6l-4-2v-4L3 5z"/></svg>
|
||||
|
After Width: | Height: | Size: 126 B |
1
frontend/dist/icons/guest.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 12c2.76 0 5-2.24 5-5S14.76 2 12 2 7 4.24 7 7s2.24 5 5 5zm0 2c-4 0-10 2-10 6v2h20v-2c0-4-6-6-10-6z"/></svg>
|
||||
|
After Width: | Height: | Size: 200 B |
1
frontend/dist/icons/layout-devices.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M4 6h18v10H4V6zm-2 12h22v2H2v-2zm4-10v6h14V8H6z"/></svg>
|
||||
|
After Width: | Height: | Size: 146 B |
1
frontend/dist/icons/layout-users.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5s-3 1.34-3 3 1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5C18 14.17 13.33 13 11 13zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.95 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 376 B |
1
frontend/dist/icons/moon.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M21 12.79A9 9 0 1111.21 3a7 7 0 109.79 9.79z"/></svg>
|
||||
|
After Width: | Height: | Size: 143 B |
1
frontend/dist/icons/settings.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M19.14 12.94a7.43 7.43 0 000-1.88l2.03-1.58a.5.5 0 00.12-.65l-1.92-3.32a.5.5 0 00-.6-.22l-2.39.96a7.36 7.36 0 00-1.63-.95L14.5 1.5a.5.5 0 00-.5-.5h-4a.5.5 0 00-.5.5l-.35 2.8a7.36 7.36 0 00-1.63.95l-2.39-.96a.5.5 0 00-.6.22L2.61 8.83a.5.5 0 00.12.65l2.03 1.58a7.43 7.43 0 000 1.88l-2.03 1.58a.5.5 0 00-.12.65l1.92 3.32a.5.5 0 00.6.22l2.39-.96c.5.39 1.05.71 1.63.95l.35 2.8a.5.5 0 00.5.5h4a.5.5 0 00.5-.5l.35-2.8c.58-.24 1.13-.56 1.63-.95l2.39.96a.5.5 0 00.6-.22l1.92-3.32a.5.5 0 00-.12-.65l-2.03-1.58zM12 15.5A3.5 3.5 0 1112 8a3.5 3.5 0 010 7.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 643 B |
1
frontend/dist/icons/sun.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6.76 4.84l-1.8-1.79-1.41 1.41 1.79 1.8 1.42-1.42zm10.45 14.32l1.79 1.8 1.41-1.41-1.8-1.79-1.4 1.4zM12 4V1h-1v3h1zm0 19v-3h-1v3h1zm8-8h3v-1h-3v1zM1 12H4v-1H1v1zm15.24-7.16l1.42 1.42 1.79-1.8-1.41-1.41-1.8 1.79zM4.22 18.36l1.42-1.42-1.8-1.79-1.41 1.41 1.79 1.8zM12 7a5 5 0 100 10 5 5 0 000-10z"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
1
frontend/dist/icons/test.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M3 13h8v-2H3v2zm0-6h14V5H3v2zm0 12h18v-2H3v2z"/></svg>
|
||||
|
After Width: | Height: | Size: 144 B |
1
frontend/dist/icons/user-plus.svg
vendored
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15 12c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM1 21v-2c0-2.76 5.82-4 9-4s9 1.24 9 4v2H1zM23 8h-2V6h-2V4h2V2h2v2h2v2h-2v2z"/></svg>
|
||||
|
After Width: | Height: | Size: 233 B |
4
frontend/dist/index.html
vendored
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>RADIUS Dashboard</title>
|
||||
<script type="module" crossorigin src="/assets/index-j5D-XEQH.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D-z4-e8u.css">
|
||||
<script type="module" crossorigin src="/assets/index-D2UhTBFe.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D8GGMeXj.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
6
frontend/node_modules/.package-lock.json
generated
vendored
@@ -998,6 +998,12 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/htm": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz",
|
||||
"integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.19",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
||||
|
||||
202
frontend/node_modules/htm/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2018 Google Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
287
frontend/node_modules/htm/README.md
generated
vendored
Normal file
@@ -0,0 +1,287 @@
|
||||
|
||||
<h1 align="center">
|
||||
HTM (Hyperscript Tagged Markup)
|
||||
<a href="https://www.npmjs.org/package/htm"><img src="https://img.shields.io/npm/v/htm.svg?style=flat" alt="npm"></a>
|
||||
</h1>
|
||||
<p align="center">
|
||||
<img src="https://i.imgur.com/0ph8dbS.png" width="572" alt="hyperscript tagged markup demo">
|
||||
</p>
|
||||
|
||||
`htm` is **JSX-like syntax in plain JavaScript** - no transpiler necessary.
|
||||
|
||||
Develop with React/Preact directly in the browser, then compile `htm` away for production.
|
||||
|
||||
It uses standard JavaScript [Tagged Templates] and works in [all modern browsers].
|
||||
|
||||
## `htm` by the numbers:
|
||||
|
||||
🐣 **< 600 bytes** when used directly in the browser
|
||||
|
||||
⚛️ **< 500 bytes** when used with Preact _(thanks gzip 🌈)_
|
||||
|
||||
🥚 **< 450 byte** `htm/mini` version
|
||||
|
||||
🏅 **0 bytes** if compiled using [babel-plugin-htm]
|
||||
|
||||
|
||||
## Syntax: like JSX but also lit
|
||||
|
||||
The syntax you write when using HTM is as close as possible to JSX:
|
||||
|
||||
- Spread props: `<div ...${props}>` instead of `<div {...props}>`
|
||||
- Self-closing tags: `<div />`
|
||||
- Components: `<${Foo}>` instead of `<Foo>` _(where `Foo` is a component reference)_
|
||||
- Boolean attributes: `<div draggable />`
|
||||
|
||||
|
||||
## Improvements over JSX
|
||||
|
||||
`htm` actually takes the JSX-style syntax a couple steps further!
|
||||
|
||||
Here's some ergonomic features you get for free that aren't present in JSX:
|
||||
|
||||
- **No transpiler necessary**
|
||||
- HTML's optional quotes: `<div class=foo>`
|
||||
- Component end-tags: `<${Footer}>footer content<//>`
|
||||
- Syntax highlighting and language support via the [lit-html VSCode extension] and [vim-jsx-pretty plugin].
|
||||
- Multiple root element (fragments): `<div /><div />`
|
||||
- Support for HTML-style comments: `<div><!-- comment --></div>`
|
||||
|
||||
## Installation
|
||||
|
||||
`htm` is published to npm, and accessible via the unpkg.com CDN:
|
||||
|
||||
**via npm:**
|
||||
|
||||
```js
|
||||
npm i htm
|
||||
```
|
||||
|
||||
**hotlinking from unpkg:** _(no build tool needed!)_
|
||||
|
||||
```js
|
||||
import htm from 'https://unpkg.com/htm?module'
|
||||
const html = htm.bind(React.createElement);
|
||||
```
|
||||
|
||||
```js
|
||||
// just want htm + preact in a single file? there's a highly-optimized version of that:
|
||||
import { html, render } from 'https://unpkg.com/htm/preact/standalone.module.js'
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
If you're using Preact or React, we've included off-the-shelf bindings to make your life easier.
|
||||
They also have the added benefit of sharing a template cache across all modules.
|
||||
|
||||
```js
|
||||
import { render } from 'preact';
|
||||
import { html } from 'htm/preact';
|
||||
render(html`<a href="/">Hello!</a>`, document.body);
|
||||
```
|
||||
|
||||
Similarly, for React:
|
||||
|
||||
```js
|
||||
import ReactDOM from 'react-dom';
|
||||
import { html } from 'htm/react';
|
||||
ReactDOM.render(html`<a href="/">Hello!</a>`, document.body);
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
Since `htm` is a generic library, we need to tell it what to "compile" our templates to.
|
||||
You can bind `htm` to any function of the form `h(type, props, ...children)` _([hyperscript])_.
|
||||
This function can return anything - `htm` never looks at the return value.
|
||||
|
||||
Here's an example `h()` function that returns tree nodes:
|
||||
|
||||
```js
|
||||
function h(type, props, ...children) {
|
||||
return { type, props, children };
|
||||
}
|
||||
```
|
||||
|
||||
To use our custom `h()` function, we need to create our own `html` tag function by binding `htm` to our `h()` function:
|
||||
|
||||
```js
|
||||
import htm from 'htm';
|
||||
|
||||
const html = htm.bind(h);
|
||||
```
|
||||
|
||||
Now we have an `html()` template tag that can be used to produce objects in the format we created above.
|
||||
|
||||
Here's the whole thing for clarity:
|
||||
|
||||
```js
|
||||
import htm from 'htm';
|
||||
|
||||
function h(type, props, ...children) {
|
||||
return { type, props, children };
|
||||
}
|
||||
|
||||
const html = htm.bind(h);
|
||||
|
||||
console.log( html`<h1 id=hello>Hello world!</h1>` );
|
||||
// {
|
||||
// type: 'h1',
|
||||
// props: { id: 'hello' },
|
||||
// children: ['Hello world!']
|
||||
// }
|
||||
```
|
||||
|
||||
If the template has multiple element at the root level
|
||||
the output is an array of `h` results:
|
||||
|
||||
```js
|
||||
console.log(html`
|
||||
<h1 id=hello>Hello</h1>
|
||||
<div class=world>World!</div>
|
||||
`);
|
||||
// [
|
||||
// {
|
||||
// type: 'h1',
|
||||
// props: { id: 'hello' },
|
||||
// children: ['Hello']
|
||||
// },
|
||||
// {
|
||||
// type: 'div',
|
||||
// props: { class: 'world' },
|
||||
// children: ['world!']
|
||||
// }
|
||||
// ]
|
||||
```
|
||||
|
||||
### Caching
|
||||
|
||||
The default build of `htm` caches template strings, which means that it can return the same Javascript object at multiple points in the tree. If you don't want this behaviour, you have three options:
|
||||
|
||||
* Change your `h` function to copy nodes when needed.
|
||||
* Add the code `this[0] = 3;` at the beginning of your `h` function, which disables caching of created elements.
|
||||
* Use `htm/mini`, which disables caching by default.
|
||||
|
||||
## Example
|
||||
|
||||
Curious to see what it all looks like? Here's a working app!
|
||||
|
||||
It's a single HTML file, and there's no build or tooling. You can edit it with nano.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<title>htm Demo</title>
|
||||
<script type="module">
|
||||
import { html, Component, render } from 'https://unpkg.com/htm/preact/standalone.module.js';
|
||||
|
||||
class App extends Component {
|
||||
addTodo() {
|
||||
const { todos = [] } = this.state;
|
||||
this.setState({ todos: todos.concat(`Item ${todos.length}`) });
|
||||
}
|
||||
render({ page }, { todos = [] }) {
|
||||
return html`
|
||||
<div class="app">
|
||||
<${Header} name="ToDo's (${page})" />
|
||||
<ul>
|
||||
${todos.map(todo => html`
|
||||
<li key=${todo}>${todo}</li>
|
||||
`)}
|
||||
</ul>
|
||||
<button onClick=${() => this.addTodo()}>Add Todo</button>
|
||||
<${Footer}>footer content here<//>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const Header = ({ name }) => html`<h1>${name} List</h1>`
|
||||
|
||||
const Footer = props => html`<footer ...${props} />`
|
||||
|
||||
render(html`<${App} page="All" />`, document.body);
|
||||
</script>
|
||||
</html>
|
||||
```
|
||||
|
||||
[⚡️ **See live version** ▶](https://htm-demo-preact.glitch.me/)
|
||||
|
||||
[⚡️ **Try this on CodeSandbox** ▶](https://codesandbox.io/s/x7pmq32j6q)
|
||||
|
||||
How nifty is that?
|
||||
|
||||
Notice there's only one import - here we're using the prebuilt Preact integration since it's easier to import and a bit smaller.
|
||||
|
||||
The same example works fine without the prebuilt version, just using two imports:
|
||||
|
||||
```js
|
||||
import { h, Component, render } from 'preact';
|
||||
import htm from 'htm';
|
||||
|
||||
const html = htm.bind(h);
|
||||
|
||||
render(html`<${App} page="All" />`, document.body);
|
||||
```
|
||||
|
||||
## Other Uses
|
||||
|
||||
Since `htm` is designed to meet the same need as JSX, you can use it anywhere you'd use JSX.
|
||||
|
||||
**Generate HTML using [vhtml]:**
|
||||
|
||||
```js
|
||||
import htm from 'htm';
|
||||
import vhtml from 'vhtml';
|
||||
|
||||
const html = htm.bind(vhtml);
|
||||
|
||||
console.log( html`<h1 id=hello>Hello world!</h1>` );
|
||||
// '<h1 id="hello">Hello world!</h1>'
|
||||
```
|
||||
|
||||
**Webpack configuration via [jsxobj]:** ([details here](https://webpack.js.org/configuration/configuration-languages/#babel-and-jsx)) _(never do this)_
|
||||
|
||||
```js
|
||||
import htm from 'htm';
|
||||
import jsxobj from 'jsxobj';
|
||||
|
||||
const html = htm.bind(jsxobj);
|
||||
|
||||
console.log(html`
|
||||
<webpack watch mode=production>
|
||||
<entry path="src/index.js" />
|
||||
</webpack>
|
||||
`);
|
||||
// {
|
||||
// watch: true,
|
||||
// mode: 'production',
|
||||
// entry: {
|
||||
// path: 'src/index.js'
|
||||
// }
|
||||
// }
|
||||
```
|
||||
|
||||
## Demos & Examples
|
||||
|
||||
- [Canadian Holidays](https://github.com/pcraig3/hols): A full app using HTM and Server-Side Rendering
|
||||
- [HTM SSR Example](https://github.com/timarney/htm-ssr-demo): Shows how to do SSR with HTM
|
||||
- [HTM + Preact SSR Demo](https://gist.github.com/developit/699c8d8f180a1e4eed58167f9c6711be)
|
||||
- [HTM + vhtml SSR Demo](https://gist.github.com/developit/ff925c3995b4a129b6b977bf7cd12ebd)
|
||||
|
||||
## Project Status
|
||||
|
||||
The original goal for `htm` was to create a wrapper around Preact that felt natural for use untranspiled in the browser. I wanted to use Virtual DOM, but I wanted to eschew build tooling and use ES Modules directly.
|
||||
|
||||
This meant giving up JSX, and the closest alternative was [Tagged Templates]. So, I wrote this library to patch up the differences between the two as much as possible. The technique turns out to be framework-agnostic, so it should work great with any library or renderer that works with JSX.
|
||||
|
||||
`htm` is stable, fast, well-tested and ready for production use.
|
||||
|
||||
[Tagged Templates]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_templates
|
||||
[lit-html]: https://github.com/Polymer/lit-html
|
||||
[babel-plugin-htm]: https://github.com/developit/htm/tree/master/packages/babel-plugin-htm
|
||||
[lit-html VSCode extension]: https://marketplace.visualstudio.com/items?itemName=bierner.lit-html
|
||||
[vim-jsx-pretty plugin]: https://github.com/MaxMEllon/vim-jsx-pretty
|
||||
[vhtml]: https://github.com/developit/vhtml
|
||||
[jsxobj]: https://github.com/developit/jsxobj
|
||||
[hyperscript]: https://github.com/hyperhype/hyperscript
|
||||
[all modern browsers]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Browser_compatibility
|
||||
6
frontend/node_modules/htm/dist/htm.d.ts
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
declare const htm: {
|
||||
bind<HResult>(
|
||||
h: (type: any, props: Record<string, any>, ...children: any[]) => HResult
|
||||
): (strings: TemplateStringsArray, ...values: any[]) => HResult | HResult[];
|
||||
};
|
||||
export default htm;
|
||||
1
frontend/node_modules/htm/dist/htm.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(){var n=function(t,e,s,u){var r;e[0]=0;for(var h=1;h<e.length;h++){var p=e[h++],a=e[h]?(e[0]|=p?1:2,s[e[h++]]):e[++h];3===p?u[0]=a:4===p?u[1]=Object.assign(u[1]||{},a):5===p?(u[1]=u[1]||{})[e[++h]]=a:6===p?u[1][e[++h]]+=a+"":p?(r=t.apply(a,n(t,a,s,["",null])),u.push(r),a[0]?e[0]|=2:(e[h-2]=0,e[h]=r)):u.push(a)}return u},t=new Map,e=function(e){var s=t.get(this);return s||(s=new Map,t.set(this,s)),(s=n(this,s.get(e)||(s.set(e,s=function(n){for(var t,e,s=1,u="",r="",h=[0],p=function(n){1===s&&(n||(u=u.replace(/^\s*\n\s*|\s*\n\s*$/g,"")))?h.push(0,n,u):3===s&&(n||u)?(h.push(3,n,u),s=2):2===s&&"..."===u&&n?h.push(4,n,0):2===s&&u&&!n?h.push(5,0,!0,u):s>=5&&((u||!n&&5===s)&&(h.push(s,0,u,e),s=6),n&&(h.push(s,n,0,e),s=6)),u=""},a=0;a<n.length;a++){a&&(1===s&&p(),p(a));for(var o=0;o<n[a].length;o++)t=n[a][o],1===s?"<"===t?(p(),h=[h],s=3):u+=t:4===s?"--"===u&&">"===t?(s=1,u=""):u=t+u[0]:r?t===r?r="":u+=t:'"'===t||"'"===t?r=t:">"===t?(p(),s=1):s&&("="===t?(s=5,e=u,u=""):"/"===t&&(s<5||">"===n[a][o+1])?(p(),3===s&&(h=h[0]),s=h,(h=h[0]).push(2,0,s),s=0):" "===t||"\t"===t||"\n"===t||"\r"===t?(p(),s=2):u+=t),3===s&&"!--"===u&&(s=4,h=h[0])}return p(),h}(e)),s),arguments,[])).length>1?s:s[0]};"undefined"!=typeof module?module.exports=e:self.htm=e}();
|
||||
1
frontend/node_modules/htm/dist/htm.mjs
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
var n=function(t,s,r,e){var u;s[0]=0;for(var h=1;h<s.length;h++){var p=s[h++],a=s[h]?(s[0]|=p?1:2,r[s[h++]]):s[++h];3===p?e[0]=a:4===p?e[1]=Object.assign(e[1]||{},a):5===p?(e[1]=e[1]||{})[s[++h]]=a:6===p?e[1][s[++h]]+=a+"":p?(u=t.apply(a,n(t,a,r,["",null])),e.push(u),a[0]?s[0]|=2:(s[h-2]=0,s[h]=u)):e.push(a)}return e},t=new Map;export default function(s){var r=t.get(this);return r||(r=new Map,t.set(this,r)),(r=n(this,r.get(s)||(r.set(s,r=function(n){for(var t,s,r=1,e="",u="",h=[0],p=function(n){1===r&&(n||(e=e.replace(/^\s*\n\s*|\s*\n\s*$/g,"")))?h.push(0,n,e):3===r&&(n||e)?(h.push(3,n,e),r=2):2===r&&"..."===e&&n?h.push(4,n,0):2===r&&e&&!n?h.push(5,0,!0,e):r>=5&&((e||!n&&5===r)&&(h.push(r,0,e,s),r=6),n&&(h.push(r,n,0,s),r=6)),e=""},a=0;a<n.length;a++){a&&(1===r&&p(),p(a));for(var l=0;l<n[a].length;l++)t=n[a][l],1===r?"<"===t?(p(),h=[h],r=3):e+=t:4===r?"--"===e&&">"===t?(r=1,e=""):e=t+e[0]:u?t===u?u="":e+=t:'"'===t||"'"===t?u=t:">"===t?(p(),r=1):r&&("="===t?(r=5,s=e,e=""):"/"===t&&(r<5||">"===n[a][l+1])?(p(),3===r&&(h=h[0]),r=h,(h=h[0]).push(2,0,r),r=0):" "===t||"\t"===t||"\n"===t||"\r"===t?(p(),r=2):e+=t),3===r&&"!--"===e&&(r=4,h=h[0])}return p(),h}(s)),r),arguments,[])).length>1?r:r[0]}
|
||||
1
frontend/node_modules/htm/dist/htm.module.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
var n=function(t,s,r,e){var u;s[0]=0;for(var h=1;h<s.length;h++){var p=s[h++],a=s[h]?(s[0]|=p?1:2,r[s[h++]]):s[++h];3===p?e[0]=a:4===p?e[1]=Object.assign(e[1]||{},a):5===p?(e[1]=e[1]||{})[s[++h]]=a:6===p?e[1][s[++h]]+=a+"":p?(u=t.apply(a,n(t,a,r,["",null])),e.push(u),a[0]?s[0]|=2:(s[h-2]=0,s[h]=u)):e.push(a)}return e},t=new Map;export default function(s){var r=t.get(this);return r||(r=new Map,t.set(this,r)),(r=n(this,r.get(s)||(r.set(s,r=function(n){for(var t,s,r=1,e="",u="",h=[0],p=function(n){1===r&&(n||(e=e.replace(/^\s*\n\s*|\s*\n\s*$/g,"")))?h.push(0,n,e):3===r&&(n||e)?(h.push(3,n,e),r=2):2===r&&"..."===e&&n?h.push(4,n,0):2===r&&e&&!n?h.push(5,0,!0,e):r>=5&&((e||!n&&5===r)&&(h.push(r,0,e,s),r=6),n&&(h.push(r,n,0,s),r=6)),e=""},a=0;a<n.length;a++){a&&(1===r&&p(),p(a));for(var l=0;l<n[a].length;l++)t=n[a][l],1===r?"<"===t?(p(),h=[h],r=3):e+=t:4===r?"--"===e&&">"===t?(r=1,e=""):e=t+e[0]:u?t===u?u="":e+=t:'"'===t||"'"===t?u=t:">"===t?(p(),r=1):r&&("="===t?(r=5,s=e,e=""):"/"===t&&(r<5||">"===n[a][l+1])?(p(),3===r&&(h=h[0]),r=h,(h=h[0]).push(2,0,r),r=0):" "===t||"\t"===t||"\n"===t||"\r"===t?(p(),r=2):e+=t),3===r&&"!--"===e&&(r=4,h=h[0])}return p(),h}(s)),r),arguments,[])).length>1?r:r[0]}
|
||||
1
frontend/node_modules/htm/dist/htm.umd.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(n,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):n.htm=e()}(this,function(){var n=function(e,t,u,s){var r;t[0]=0;for(var p=1;p<t.length;p++){var h=t[p++],o=t[p]?(t[0]|=h?1:2,u[t[p++]]):t[++p];3===h?s[0]=o:4===h?s[1]=Object.assign(s[1]||{},o):5===h?(s[1]=s[1]||{})[t[++p]]=o:6===h?s[1][t[++p]]+=o+"":h?(r=e.apply(o,n(e,o,u,["",null])),s.push(r),o[0]?t[0]|=2:(t[p-2]=0,t[p]=r)):s.push(o)}return s},e=new Map;return function(t){var u=e.get(this);return u||(u=new Map,e.set(this,u)),(u=n(this,u.get(t)||(u.set(t,u=function(n){for(var e,t,u=1,s="",r="",p=[0],h=function(n){1===u&&(n||(s=s.replace(/^\s*\n\s*|\s*\n\s*$/g,"")))?p.push(0,n,s):3===u&&(n||s)?(p.push(3,n,s),u=2):2===u&&"..."===s&&n?p.push(4,n,0):2===u&&s&&!n?p.push(5,0,!0,s):u>=5&&((s||!n&&5===u)&&(p.push(u,0,s,t),u=6),n&&(p.push(u,n,0,t),u=6)),s=""},o=0;o<n.length;o++){o&&(1===u&&h(),h(o));for(var f=0;f<n[o].length;f++)e=n[o][f],1===u?"<"===e?(h(),p=[p],u=3):s+=e:4===u?"--"===s&&">"===e?(u=1,s=""):s=e+s[0]:r?e===r?r="":s+=e:'"'===e||"'"===e?r=e:">"===e?(h(),u=1):u&&("="===e?(u=5,t=s,s=""):"/"===e&&(u<5||">"===n[o][f+1])?(h(),3===u&&(p=p[0]),u=p,(p=p[0]).push(2,0,u),u=0):" "===e||"\t"===e||"\n"===e||"\r"===e?(h(),u=2):s+=e),3===u&&"!--"===s&&(u=4,p=p[0])}return h(),p}(t)),u),arguments,[])).length>1?u:u[0]}});
|
||||
6
frontend/node_modules/htm/mini/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
declare const htm: {
|
||||
bind<HResult>(
|
||||
h: (type: any, props: Record<string, any>, ...children: any[]) => HResult
|
||||
): (strings: TemplateStringsArray, ...values: any[]) => HResult | HResult[];
|
||||
};
|
||||
export default htm;
|
||||
1
frontend/node_modules/htm/mini/index.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(){var n=(new Map,function(n){for(var e,l,s=arguments,t=1,u="",r="",o=[0],f=function(n){1===t&&(n||(u=u.replace(/^\s*\n\s*|\s*\n\s*$/g,"")))?o.push(n?s[n]:u):3===t&&(n||u)?(o[1]=n?s[n]:u,t=2):2===t&&"..."===u&&n?o[2]=Object.assign(o[2]||{},s[n]):2===t&&u&&!n?(o[2]=o[2]||{})[u]=!0:t>=5&&(5===t?((o[2]=o[2]||{})[l]=n?u?u+s[n]:s[n]:u,t=6):(n||u)&&(o[2][l]+=n?u+s[n]:u)),u=""},i=0;i<n.length;i++){i&&(1===t&&f(),f(i));for(var p=0;p<n[i].length;p++)e=n[i][p],1===t?"<"===e?(f(),o=[o,"",null],t=3):u+=e:4===t?"--"===u&&">"===e?(t=1,u=""):u=e+u[0]:r?e===r?r="":u+=e:'"'===e||"'"===e?r=e:">"===e?(f(),t=1):t&&("="===e?(t=5,l=u,u=""):"/"===e&&(t<5||">"===n[i][p+1])?(f(),3===t&&(o=o[0]),t=o,(o=o[0]).push(this.apply(null,t.slice(1))),t=0):" "===e||"\t"===e||"\n"===e||"\r"===e?(f(),t=2):u+=e),3===t&&"!--"===u&&(t=4,o=o[0])}return f(),o.length>2?o.slice(1):o[1]});"undefined"!=typeof module?module.exports=n:self.htm=n}();
|
||||
1
frontend/node_modules/htm/mini/index.mjs
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export default function(n){for(var l,e,s=arguments,t=1,r="",u="",a=[0],c=function(n){1===t&&(n||(r=r.replace(/^\s*\n\s*|\s*\n\s*$/g,"")))?a.push(n?s[n]:r):3===t&&(n||r)?(a[1]=n?s[n]:r,t=2):2===t&&"..."===r&&n?a[2]=Object.assign(a[2]||{},s[n]):2===t&&r&&!n?(a[2]=a[2]||{})[r]=!0:t>=5&&(5===t?((a[2]=a[2]||{})[e]=n?r?r+s[n]:s[n]:r,t=6):(n||r)&&(a[2][e]+=n?r+s[n]:r)),r=""},h=0;h<n.length;h++){h&&(1===t&&c(),c(h));for(var i=0;i<n[h].length;i++)l=n[h][i],1===t?"<"===l?(c(),a=[a,"",null],t=3):r+=l:4===t?"--"===r&&">"===l?(t=1,r=""):r=l+r[0]:u?l===u?u="":r+=l:'"'===l||"'"===l?u=l:">"===l?(c(),t=1):t&&("="===l?(t=5,e=r,r=""):"/"===l&&(t<5||">"===n[h][i+1])?(c(),3===t&&(a=a[0]),t=a,(a=a[0]).push(this.apply(null,t.slice(1))),t=0):" "===l||"\t"===l||"\n"===l||"\r"===l?(c(),t=2):r+=l),3===t&&"!--"===r&&(t=4,a=a[0])}return c(),a.length>2?a.slice(1):a[1]}
|
||||
1
frontend/node_modules/htm/mini/index.module.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export default function(n){for(var l,e,s=arguments,t=1,r="",u="",a=[0],c=function(n){1===t&&(n||(r=r.replace(/^\s*\n\s*|\s*\n\s*$/g,"")))?a.push(n?s[n]:r):3===t&&(n||r)?(a[1]=n?s[n]:r,t=2):2===t&&"..."===r&&n?a[2]=Object.assign(a[2]||{},s[n]):2===t&&r&&!n?(a[2]=a[2]||{})[r]=!0:t>=5&&(5===t?((a[2]=a[2]||{})[e]=n?r?r+s[n]:s[n]:r,t=6):(n||r)&&(a[2][e]+=n?r+s[n]:r)),r=""},h=0;h<n.length;h++){h&&(1===t&&c(),c(h));for(var i=0;i<n[h].length;i++)l=n[h][i],1===t?"<"===l?(c(),a=[a,"",null],t=3):r+=l:4===t?"--"===r&&">"===l?(t=1,r=""):r=l+r[0]:u?l===u?u="":r+=l:'"'===l||"'"===l?u=l:">"===l?(c(),t=1):t&&("="===l?(t=5,e=r,r=""):"/"===l&&(t<5||">"===n[h][i+1])?(c(),3===t&&(a=a[0]),t=a,(a=a[0]).push(this.apply(null,t.slice(1))),t=0):" "===l||"\t"===l||"\n"===l||"\r"===l?(c(),t=2):r+=l),3===t&&"!--"===r&&(t=4,a=a[0])}return c(),a.length>2?a.slice(1):a[1]}
|
||||
1
frontend/node_modules/htm/mini/index.umd.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):e.htm=n()}(this,function(){return function(e){for(var n,t,o=arguments,f=1,i="",s="",u=[0],l=function(e){1===f&&(e||(i=i.replace(/^\s*\n\s*|\s*\n\s*$/g,"")))?u.push(e?o[e]:i):3===f&&(e||i)?(u[1]=e?o[e]:i,f=2):2===f&&"..."===i&&e?u[2]=Object.assign(u[2]||{},o[e]):2===f&&i&&!e?(u[2]=u[2]||{})[i]=!0:f>=5&&(5===f?((u[2]=u[2]||{})[t]=e?i?i+o[e]:o[e]:i,f=6):(e||i)&&(u[2][t]+=e?i+o[e]:i)),i=""},r=0;r<e.length;r++){r&&(1===f&&l(),l(r));for(var c=0;c<e[r].length;c++)n=e[r][c],1===f?"<"===n?(l(),u=[u,"",null],f=3):i+=n:4===f?"--"===i&&">"===n?(f=1,i=""):i=n+i[0]:s?n===s?s="":i+=n:'"'===n||"'"===n?s=n:">"===n?(l(),f=1):f&&("="===n?(f=5,t=i,i=""):"/"===n&&(f<5||">"===e[r][c+1])?(l(),3===f&&(u=u[0]),f=u,(u=u[0]).push(this.apply(null,f.slice(1))),f=0):" "===n||"\t"===n||"\n"===n||"\r"===n?(l(),f=2):i+=n),3===f&&"!--"===i&&(f=4,u=u[0])}return l(),u.length>2?u.slice(1):u[1]}});
|
||||
124
frontend/node_modules/htm/package.json
generated
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"name": "htm",
|
||||
"version": "3.1.1",
|
||||
"description": "The Tagged Template syntax for Virtual DOM. Only browser-compatible syntax.",
|
||||
"main": "dist/htm.js",
|
||||
"umd:main": "dist/htm.umd.js",
|
||||
"module": "dist/htm.module.js",
|
||||
"types": "dist/htm.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/htm.d.ts",
|
||||
"browser": "./dist/htm.module.js",
|
||||
"umd": "./dist/htm.umd.js",
|
||||
"import": "./dist/htm.mjs",
|
||||
"require": "./dist/htm.js"
|
||||
},
|
||||
"./": "./",
|
||||
"./preact": {
|
||||
"types": "./preact/index.d.ts",
|
||||
"browser": "./preact/index.module.js",
|
||||
"umd": "./preact/index.umd.js",
|
||||
"import": "./preact/index.mjs",
|
||||
"require": "./preact/index.js"
|
||||
},
|
||||
"./preact/standalone": {
|
||||
"types": "./preact/index.d.ts",
|
||||
"browser": "./preact/standalone.module.js",
|
||||
"umd": "./preact/standalone.umd.js",
|
||||
"import": "./preact/standalone.mjs",
|
||||
"require": "./preact/standalone.js"
|
||||
},
|
||||
"./react": {
|
||||
"types": "./react/index.d.ts",
|
||||
"browser": "./react/index.module.js",
|
||||
"umd": "./react/index.umd.js",
|
||||
"import": "./react/index.mjs",
|
||||
"require": "./react/index.js"
|
||||
},
|
||||
"./mini": {
|
||||
"types": "./mini/index.d.ts",
|
||||
"browser": "./mini/index.module.js",
|
||||
"umd": "./mini/index.umd.js",
|
||||
"import": "./mini/index.mjs",
|
||||
"require": "./mini/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run -s build:main && npm run -s build:mini && npm run -s build:preact && npm run -s build:react && npm run -s build:babel && npm run -s build:babel-transform-jsx && npm run -s build:mjsalias",
|
||||
"build:main": "microbundle src/index.mjs -f es,umd --no-sourcemap --target web && microbundle src/cjs.mjs -f iife --no-sourcemap --target web && cp src/index.d.ts dist/htm.d.ts",
|
||||
"build:mini": "microbundle src/index.mjs -o ./mini/index.js -f es,umd --no-sourcemap --target web --alias ./constants.mjs=./constants-mini.mjs && microbundle src/cjs.mjs -o ./mini/index.js -f iife --no-sourcemap --target web --alias ./constants.mjs=./constants-mini.mjs && cp src/index.d.ts mini",
|
||||
"build:preact": "cd src/integrations/preact && npm run build",
|
||||
"build:react": "cd src/integrations/react && npm run build",
|
||||
"build:babel": "cd packages/babel-plugin-htm && npm run build",
|
||||
"build:babel-transform-jsx": "cd packages/babel-plugin-transform-jsx-to-htm && npm run build",
|
||||
"build:mjsalias": "cp dist/htm.module.js dist/htm.mjs && cp mini/index.module.js mini/index.mjs && cp preact/index.module.js preact/index.mjs && cp preact/standalone.module.js preact/standalone.mjs && cp react/index.module.js react/index.mjs",
|
||||
"test": "eslint src/**/*.mjs test/**/*.mjs --ignore-path .gitignore && npm run build && jest test",
|
||||
"test:perf": "v8 test/__perftest.mjs",
|
||||
"test:dist": "npm pack && mv htm*.tgz test/fixtures/esm/htm.tgz && cd test/fixtures/esm && npm install && node index.js",
|
||||
"release": "npm t && git commit -am \"$npm_package_version\" && git tag $npm_package_version && git push && git push --tags && npm publish"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"mini",
|
||||
"preact",
|
||||
"react",
|
||||
"src"
|
||||
],
|
||||
"eslintConfig": {
|
||||
"extends": "developit",
|
||||
"rules": {
|
||||
"prefer-const": 0,
|
||||
"prefer-spread": 0,
|
||||
"prefer-rest-params": 0,
|
||||
"func-style": 0
|
||||
}
|
||||
},
|
||||
"jest": {
|
||||
"testURL": "http://localhost",
|
||||
"testMatch": [
|
||||
"**/__tests__/**/*.?(m)js?(x)",
|
||||
"**/?(*.)(spec|test).?(m)js?(x)"
|
||||
],
|
||||
"transform": {
|
||||
"\\.m?js$": "babel-jest"
|
||||
},
|
||||
"moduleFileExtensions": [
|
||||
"mjs",
|
||||
"js"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^babel-plugin-transform-jsx-to-htm$": "<rootDir>/packages/babel-plugin-transform-jsx-to-htm/index.mjs",
|
||||
"^babel-plugin-htm$": "<rootDir>/packages/babel-plugin-htm/index.mjs",
|
||||
"^htm$": "<rootDir>/src/index.mjs",
|
||||
"^htm/preact$": "<rootDir>/src/integrations/preact/index.mjs"
|
||||
}
|
||||
},
|
||||
"repository": "developit/htm",
|
||||
"keywords": [
|
||||
"Hyperscript Tagged Markup",
|
||||
"tagged template",
|
||||
"template literals",
|
||||
"html",
|
||||
"htm",
|
||||
"jsx",
|
||||
"virtual dom",
|
||||
"hyperscript"
|
||||
],
|
||||
"author": "Jason Miller <jason@developit.ca>",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://github.com/developit/htm",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.2.2",
|
||||
"@babel/preset-env": "^7.1.6",
|
||||
"@types/jest": "^26.0.24",
|
||||
"babel-jest": "^24.1.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"eslint": "^5.2.0",
|
||||
"eslint-config-developit": "^1.1.1",
|
||||
"jest": "^24.1.0",
|
||||
"microbundle": "^0.10.1",
|
||||
"preact": "^10.2.0",
|
||||
"react": "^16.8.3"
|
||||
}
|
||||
}
|
||||
5
frontend/node_modules/htm/preact/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import { h, VNode, Component } from 'preact';
|
||||
export * from 'preact/hooks';
|
||||
declare function render(tree: VNode, parent: HTMLElement): void;
|
||||
declare const html: (strings: TemplateStringsArray, ...values: any[]) => VNode;
|
||||
export { h, html, render, Component };
|
||||
1
frontend/node_modules/htm/preact/index.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
var e,r=require("preact"),t=((e=require("htm"))&&"object"==typeof e&&"default"in e?e.default:e).bind(r.h);exports.h=r.h,exports.render=r.render,exports.Component=r.Component,exports.html=t;
|
||||
1
frontend/node_modules/htm/preact/index.mjs
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{h as r,Component as o,render as t}from"preact";export{h,render,Component}from"preact";import e from"htm";var m=e.bind(r);export{m as html};
|
||||
1
frontend/node_modules/htm/preact/index.module.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{h as r,Component as o,render as t}from"preact";export{h,render,Component}from"preact";import e from"htm";var m=e.bind(r);export{m as html};
|
||||
1
frontend/node_modules/htm/preact/index.umd.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("preact"),require("htm")):"function"==typeof define&&define.amd?define(["exports","preact","htm"],t):t(e.htmPreact={},e.preact,e.htm)}(this,function(e,t,n){var r=(n=n&&n.hasOwnProperty("default")?n.default:n).bind(t.h);e.h=t.h,e.render=t.render,e.Component=t.Component,e.html=r});
|
||||
13
frontend/node_modules/htm/preact/package.json
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "htm_preact",
|
||||
"module": "index.module.js",
|
||||
"main": "index.js",
|
||||
"umd:main": "index.umd.js",
|
||||
"unpkg": "standalone.js",
|
||||
"scripts": {
|
||||
"build": "npm run -s build:main && npm run -s build:standalone && npm run -s build:static",
|
||||
"build:main": "microbundle index.mjs -o ../../../preact/index.js --external preact,htm --no-sourcemap --target web",
|
||||
"build:static": "cp index.d.ts package.json ../../../preact/",
|
||||
"build:standalone": "microbundle standalone.mjs -o ../../../preact/standalone.js -f es,umd --no-sourcemap --target web"
|
||||
}
|
||||
}
|
||||
1
frontend/node_modules/htm/preact/standalone.mjs
generated
vendored
Normal file
1
frontend/node_modules/htm/preact/standalone.module.js
generated
vendored
Normal file
1
frontend/node_modules/htm/preact/standalone.umd.js
generated
vendored
Normal file
2
frontend/node_modules/htm/react/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import * as React from 'react';
|
||||
declare const html: (strings: TemplateStringsArray, ...values: any[]) => React.ReactElement;
|
||||
1
frontend/node_modules/htm/react/index.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
var e,t=require("react"),r=((e=require("htm"))&&"object"==typeof e&&"default"in e?e.default:e).bind(t.createElement);exports.html=r;
|
||||
1
frontend/node_modules/htm/react/index.mjs
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{createElement as r}from"react";import m from"htm";var o=m.bind(r);export{o as html};
|
||||
1
frontend/node_modules/htm/react/index.module.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
import{createElement as r}from"react";import m from"htm";var o=m.bind(r);export{o as html};
|
||||
1
frontend/node_modules/htm/react/index.umd.js
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("react"),require("htm")):"function"==typeof define&&define.amd?define(["exports","react","htm"],t):t(e.htmReact={},e.react,e.htm)}(this,function(e,t,n){var r=(n=n&&n.hasOwnProperty("default")?n.default:n).bind(t.createElement);e.html=r});
|
||||
12
frontend/node_modules/htm/react/package.json
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "htm_react",
|
||||
"module": "index.module.js",
|
||||
"main": "index.js",
|
||||
"umd:main": "index.umd.js",
|
||||
"unpkg": "index.js",
|
||||
"scripts": {
|
||||
"build": "npm run -s build:main && npm run -s build:static",
|
||||
"build:main": "microbundle index.mjs -o ../../../react/index.js --external react,htm --no-sourcemap --target web",
|
||||
"build:static": "cp index.d.ts package.json ../../../react/"
|
||||
}
|
||||
}
|
||||
292
frontend/node_modules/htm/src/build.mjs
generated
vendored
Normal file
@@ -0,0 +1,292 @@
|
||||
import { MINI } from './constants.mjs';
|
||||
|
||||
const MODE_SLASH = 0;
|
||||
const MODE_TEXT = 1;
|
||||
const MODE_WHITESPACE = 2;
|
||||
const MODE_TAGNAME = 3;
|
||||
const MODE_COMMENT = 4;
|
||||
const MODE_PROP_SET = 5;
|
||||
const MODE_PROP_APPEND = 6;
|
||||
|
||||
const CHILD_APPEND = 0;
|
||||
const CHILD_RECURSE = 2;
|
||||
const TAG_SET = 3;
|
||||
const PROPS_ASSIGN = 4;
|
||||
const PROP_SET = MODE_PROP_SET;
|
||||
const PROP_APPEND = MODE_PROP_APPEND;
|
||||
|
||||
// Turn a result of a build(...) call into a tree that is more
|
||||
// convenient to analyze and transform (e.g. Babel plugins).
|
||||
// For example:
|
||||
// treeify(
|
||||
// build`<div href="1${a}" ...${b}><${x} /></div>`,
|
||||
// [X, Y, Z]
|
||||
// )
|
||||
// returns:
|
||||
// {
|
||||
// tag: 'div',
|
||||
// props: [ { href: ["1", X] }, Y ],
|
||||
// children: [ { tag: Z, props: [], children: [] } ]
|
||||
// }
|
||||
export const treeify = (built, fields) => {
|
||||
const _treeify = built => {
|
||||
let tag = '';
|
||||
let currentProps = null;
|
||||
const props = [];
|
||||
const children = [];
|
||||
|
||||
for (let i = 1; i < built.length; i++) {
|
||||
const type = built[i++];
|
||||
const value = built[i] ? fields[built[i++]-1] : built[++i];
|
||||
|
||||
if (type === TAG_SET) {
|
||||
tag = value;
|
||||
}
|
||||
else if (type === PROPS_ASSIGN) {
|
||||
props.push(value);
|
||||
currentProps = null;
|
||||
}
|
||||
else if (type === PROP_SET) {
|
||||
if (!currentProps) {
|
||||
currentProps = Object.create(null);
|
||||
props.push(currentProps);
|
||||
}
|
||||
currentProps[built[++i]] = [value];
|
||||
}
|
||||
else if (type === PROP_APPEND) {
|
||||
currentProps[built[++i]].push(value);
|
||||
}
|
||||
else if (type === CHILD_RECURSE) {
|
||||
children.push(_treeify(value));
|
||||
}
|
||||
else if (type === CHILD_APPEND) {
|
||||
children.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
return { tag, props, children };
|
||||
};
|
||||
const { children } = _treeify(built);
|
||||
return children.length > 1 ? children : children[0];
|
||||
};
|
||||
|
||||
export const evaluate = (h, built, fields, args) => {
|
||||
let tmp;
|
||||
|
||||
// `build()` used the first element of the operation list as
|
||||
// temporary workspace. Now that `build()` is done we can use
|
||||
// that space to track whether the current element is "dynamic"
|
||||
// (i.e. it or any of its descendants depend on dynamic values).
|
||||
built[0] = 0;
|
||||
|
||||
for (let i = 1; i < built.length; i++) {
|
||||
const type = built[i++];
|
||||
|
||||
// Set `built[0]`'s appropriate bits if this element depends on a dynamic value.
|
||||
const value = built[i] ? ((built[0] |= type ? 1 : 2), fields[built[i++]]) : built[++i];
|
||||
|
||||
if (type === TAG_SET) {
|
||||
args[0] = value;
|
||||
}
|
||||
else if (type === PROPS_ASSIGN) {
|
||||
args[1] = Object.assign(args[1] || {}, value);
|
||||
}
|
||||
else if (type === PROP_SET) {
|
||||
(args[1] = args[1] || {})[built[++i]] = value;
|
||||
}
|
||||
else if (type === PROP_APPEND) {
|
||||
args[1][built[++i]] += (value + '');
|
||||
}
|
||||
else if (type) { // type === CHILD_RECURSE
|
||||
// Set the operation list (including the staticness bits) as
|
||||
// `this` for the `h` call.
|
||||
tmp = h.apply(value, evaluate(h, value, fields, ['', null]));
|
||||
args.push(tmp);
|
||||
|
||||
if (value[0]) {
|
||||
// Set the 2nd lowest bit it the child element is dynamic.
|
||||
built[0] |= 2;
|
||||
}
|
||||
else {
|
||||
// Rewrite the operation list in-place if the child element is static.
|
||||
// The currently evaluated piece `CHILD_RECURSE, 0, [...]` becomes
|
||||
// `CHILD_APPEND, 0, tmp`.
|
||||
// Essentially the operation list gets optimized for potential future
|
||||
// re-evaluations.
|
||||
built[i-2] = CHILD_APPEND;
|
||||
built[i] = tmp;
|
||||
}
|
||||
}
|
||||
else { // type === CHILD_APPEND
|
||||
args.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
};
|
||||
|
||||
export const build = function(statics) {
|
||||
const fields = arguments;
|
||||
const h = this;
|
||||
|
||||
let mode = MODE_TEXT;
|
||||
let buffer = '';
|
||||
let quote = '';
|
||||
let current = [0];
|
||||
let char, propName;
|
||||
|
||||
const commit = field => {
|
||||
if (mode === MODE_TEXT && (field || (buffer = buffer.replace(/^\s*\n\s*|\s*\n\s*$/g,'')))) {
|
||||
if (MINI) {
|
||||
current.push(field ? fields[field] : buffer);
|
||||
}
|
||||
else {
|
||||
current.push(CHILD_APPEND, field, buffer);
|
||||
}
|
||||
}
|
||||
else if (mode === MODE_TAGNAME && (field || buffer)) {
|
||||
if (MINI) {
|
||||
current[1] = field ? fields[field] : buffer;
|
||||
}
|
||||
else {
|
||||
current.push(TAG_SET, field, buffer);
|
||||
}
|
||||
mode = MODE_WHITESPACE;
|
||||
}
|
||||
else if (mode === MODE_WHITESPACE && buffer === '...' && field) {
|
||||
if (MINI) {
|
||||
current[2] = Object.assign(current[2] || {}, fields[field]);
|
||||
}
|
||||
else {
|
||||
current.push(PROPS_ASSIGN, field, 0);
|
||||
}
|
||||
}
|
||||
else if (mode === MODE_WHITESPACE && buffer && !field) {
|
||||
if (MINI) {
|
||||
(current[2] = current[2] || {})[buffer] = true;
|
||||
}
|
||||
else {
|
||||
current.push(PROP_SET, 0, true, buffer);
|
||||
}
|
||||
}
|
||||
else if (mode >= MODE_PROP_SET) {
|
||||
if (MINI) {
|
||||
if (mode === MODE_PROP_SET) {
|
||||
(current[2] = current[2] || {})[propName] = field ? buffer ? (buffer + fields[field]) : fields[field] : buffer;
|
||||
mode = MODE_PROP_APPEND;
|
||||
}
|
||||
else if (field || buffer) {
|
||||
current[2][propName] += field ? buffer + fields[field] : buffer;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (buffer || (!field && mode === MODE_PROP_SET)) {
|
||||
current.push(mode, 0, buffer, propName);
|
||||
mode = MODE_PROP_APPEND;
|
||||
}
|
||||
if (field) {
|
||||
current.push(mode, field, 0, propName);
|
||||
mode = MODE_PROP_APPEND;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buffer = '';
|
||||
};
|
||||
|
||||
for (let i=0; i<statics.length; i++) {
|
||||
if (i) {
|
||||
if (mode === MODE_TEXT) {
|
||||
commit();
|
||||
}
|
||||
commit(i);
|
||||
}
|
||||
|
||||
for (let j=0; j<statics[i].length;j++) {
|
||||
char = statics[i][j];
|
||||
|
||||
if (mode === MODE_TEXT) {
|
||||
if (char === '<') {
|
||||
// commit buffer
|
||||
commit();
|
||||
if (MINI) {
|
||||
current = [current, '', null];
|
||||
}
|
||||
else {
|
||||
current = [current];
|
||||
}
|
||||
mode = MODE_TAGNAME;
|
||||
}
|
||||
else {
|
||||
buffer += char;
|
||||
}
|
||||
}
|
||||
else if (mode === MODE_COMMENT) {
|
||||
// Ignore everything until the last three characters are '-', '-' and '>'
|
||||
if (buffer === '--' && char === '>') {
|
||||
mode = MODE_TEXT;
|
||||
buffer = '';
|
||||
}
|
||||
else {
|
||||
buffer = char + buffer[0];
|
||||
}
|
||||
}
|
||||
else if (quote) {
|
||||
if (char === quote) {
|
||||
quote = '';
|
||||
}
|
||||
else {
|
||||
buffer += char;
|
||||
}
|
||||
}
|
||||
else if (char === '"' || char === "'") {
|
||||
quote = char;
|
||||
}
|
||||
else if (char === '>') {
|
||||
commit();
|
||||
mode = MODE_TEXT;
|
||||
}
|
||||
else if (!mode) {
|
||||
// Ignore everything until the tag ends
|
||||
}
|
||||
else if (char === '=') {
|
||||
mode = MODE_PROP_SET;
|
||||
propName = buffer;
|
||||
buffer = '';
|
||||
}
|
||||
else if (char === '/' && (mode < MODE_PROP_SET || statics[i][j+1] === '>')) {
|
||||
commit();
|
||||
if (mode === MODE_TAGNAME) {
|
||||
current = current[0];
|
||||
}
|
||||
mode = current;
|
||||
if (MINI) {
|
||||
(current = current[0]).push(h.apply(null, mode.slice(1)));
|
||||
}
|
||||
else {
|
||||
(current = current[0]).push(CHILD_RECURSE, 0, mode);
|
||||
}
|
||||
mode = MODE_SLASH;
|
||||
}
|
||||
else if (char === ' ' || char === '\t' || char === '\n' || char === '\r') {
|
||||
// <a disabled>
|
||||
commit();
|
||||
mode = MODE_WHITESPACE;
|
||||
}
|
||||
else {
|
||||
buffer += char;
|
||||
}
|
||||
|
||||
if (mode === MODE_TAGNAME && buffer === '!--') {
|
||||
mode = MODE_COMMENT;
|
||||
current = current[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
commit();
|
||||
|
||||
if (MINI) {
|
||||
return current.length > 2 ? current.slice(1) : current[1];
|
||||
}
|
||||
return current;
|
||||
};
|
||||
3
frontend/node_modules/htm/src/cjs.mjs
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import htm from './index.mjs';
|
||||
if (typeof module != 'undefined') module.exports = htm;
|
||||
else self.htm = htm;
|
||||
1
frontend/node_modules/htm/src/constants-mini.mjs
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export const MINI = true;
|
||||
1
frontend/node_modules/htm/src/constants.mjs
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export const MINI = false;
|
||||
6
frontend/node_modules/htm/src/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
declare const htm: {
|
||||
bind<HResult>(
|
||||
h: (type: any, props: Record<string, any>, ...children: any[]) => HResult
|
||||
): (strings: TemplateStringsArray, ...values: any[]) => HResult | HResult[];
|
||||
};
|
||||
export default htm;
|
||||
29
frontend/node_modules/htm/src/index.mjs
generated
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { MINI } from './constants.mjs';
|
||||
import { build, evaluate } from './build.mjs';
|
||||
|
||||
const CACHES = new Map();
|
||||
|
||||
const regular = function(statics) {
|
||||
let tmp = CACHES.get(this);
|
||||
if (!tmp) {
|
||||
tmp = new Map();
|
||||
CACHES.set(this, tmp);
|
||||
}
|
||||
tmp = evaluate(this, tmp.get(statics) || (tmp.set(statics, tmp = build(statics)), tmp), arguments, []);
|
||||
return tmp.length > 1 ? tmp : tmp[0];
|
||||
};
|
||||
|
||||
export default MINI ? build : regular;
|
||||
5
frontend/node_modules/htm/src/integrations/preact/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import { h, VNode, Component } from 'preact';
|
||||
export * from 'preact/hooks';
|
||||
declare function render(tree: VNode, parent: HTMLElement): void;
|
||||
declare const html: (strings: TemplateStringsArray, ...values: any[]) => VNode;
|
||||
export { h, html, render, Component };
|
||||
19
frontend/node_modules/htm/src/integrations/preact/index.mjs
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { h, Component, render } from 'preact';
|
||||
import htm from 'htm';
|
||||
|
||||
const html = htm.bind(h);
|
||||
|
||||
export { h, html, render, Component };
|
||||
13
frontend/node_modules/htm/src/integrations/preact/package.json
generated
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "htm_preact",
|
||||
"module": "index.module.js",
|
||||
"main": "index.js",
|
||||
"umd:main": "index.umd.js",
|
||||
"unpkg": "standalone.js",
|
||||
"scripts": {
|
||||
"build": "npm run -s build:main && npm run -s build:standalone && npm run -s build:static",
|
||||
"build:main": "microbundle index.mjs -o ../../../preact/index.js --external preact,htm --no-sourcemap --target web",
|
||||
"build:static": "cp index.d.ts package.json ../../../preact/",
|
||||
"build:standalone": "microbundle standalone.mjs -o ../../../preact/standalone.js -f es,umd --no-sourcemap --target web"
|
||||
}
|
||||
}
|
||||
20
frontend/node_modules/htm/src/integrations/preact/standalone.mjs
generated
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { h, Component, createContext, render } from 'preact';
|
||||
import { useState, useReducer, useEffect, useLayoutEffect, useRef, useImperativeHandle, useMemo, useCallback, useContext, useDebugValue, useErrorBoundary } from 'preact/hooks';
|
||||
import htm from '../../index.mjs';
|
||||
|
||||
const html = htm.bind(h);
|
||||
|
||||
export { h, html, render, Component, createContext, useState, useReducer, useEffect, useLayoutEffect, useRef, useImperativeHandle, useMemo, useCallback, useContext, useDebugValue, useErrorBoundary };
|
||||
2
frontend/node_modules/htm/src/integrations/react/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
import * as React from 'react';
|
||||
declare const html: (strings: TemplateStringsArray, ...values: any[]) => React.ReactElement;
|
||||
16
frontend/node_modules/htm/src/integrations/react/index.mjs
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright 2018 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createElement } from 'react';
|
||||
import htm from 'htm';
|
||||
export const html = htm.bind(createElement);
|
||||
12
frontend/node_modules/htm/src/integrations/react/package.json
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "htm_react",
|
||||
"module": "index.module.js",
|
||||
"main": "index.js",
|
||||
"umd:main": "index.umd.js",
|
||||
"unpkg": "index.js",
|
||||
"scripts": {
|
||||
"build": "npm run -s build:main && npm run -s build:static",
|
||||
"build:main": "microbundle index.mjs -o ../../../react/index.js --external react,htm --no-sourcemap --target web",
|
||||
"build:static": "cp index.d.ts package.json ../../../react/"
|
||||
}
|
||||
}
|
||||
7
frontend/package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "radius-frontend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"htm": "^3.1.1",
|
||||
"vue": "^3.4.38"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -965,6 +966,12 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/htm": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz",
|
||||
"integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.19",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.38"
|
||||
"vue": "^3.4.38",
|
||||
"htm": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"vite": "^5.4.8"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1
frontend/public/icons/clear.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6 19a2 2 0 002 2h8a2 2 0 002-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
||||
|
After Width: | Height: | Size: 171 B |
1
frontend/public/icons/copy.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
|
||||
|
After Width: | Height: | Size: 226 B |
1
frontend/public/icons/download.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5 20h14v-2H5v2zM11 3h2v8h3l-4 4-4-4h3V3z"/></svg>
|
||||
|
After Width: | Height: | Size: 140 B |
1
frontend/public/icons/filter.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M3 5h18l-7 8v6l-4-2v-4L3 5z"/></svg>
|
||||
|
After Width: | Height: | Size: 126 B |
1
frontend/public/icons/guest.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 12c2.76 0 5-2.24 5-5S14.76 2 12 2 7 4.24 7 7s2.24 5 5 5zm0 2c-4 0-10 2-10 6v2h20v-2c0-4-6-6-10-6z"/></svg>
|
||||
|
After Width: | Height: | Size: 200 B |
1
frontend/public/icons/layout-devices.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M4 6h18v10H4V6zm-2 12h22v2H2v-2zm4-10v6h14V8H6z"/></svg>
|
||||
|
After Width: | Height: | Size: 146 B |
1
frontend/public/icons/layout-users.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5s-3 1.34-3 3 1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5C18 14.17 13.33 13 11 13zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.95 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 376 B |
1
frontend/public/icons/moon.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M21 12.79A9 9 0 1111.21 3a7 7 0 109.79 9.79z"/></svg>
|
||||
|
After Width: | Height: | Size: 143 B |
1
frontend/public/icons/settings.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M19.14 12.94a7.43 7.43 0 000-1.88l2.03-1.58a.5.5 0 00.12-.65l-1.92-3.32a.5.5 0 00-.6-.22l-2.39.96a7.36 7.36 0 00-1.63-.95L14.5 1.5a.5.5 0 00-.5-.5h-4a.5.5 0 00-.5.5l-.35 2.8a7.36 7.36 0 00-1.63.95l-2.39-.96a.5.5 0 00-.6.22L2.61 8.83a.5.5 0 00.12.65l2.03 1.58a7.43 7.43 0 000 1.88l-2.03 1.58a.5.5 0 00-.12.65l1.92 3.32a.5.5 0 00.6.22l2.39-.96c.5.39 1.05.71 1.63.95l.35 2.8a.5.5 0 00.5.5h4a.5.5 0 00.5-.5l.35-2.8c.58-.24 1.13-.56 1.63-.95l2.39.96a.5.5 0 00.6-.22l1.92-3.32a.5.5 0 00-.12-.65l-2.03-1.58zM12 15.5A3.5 3.5 0 1112 8a3.5 3.5 0 010 7.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 643 B |
1
frontend/public/icons/sun.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M6.76 4.84l-1.8-1.79-1.41 1.41 1.79 1.8 1.42-1.42zm10.45 14.32l1.79 1.8 1.41-1.41-1.8-1.79-1.4 1.4zM12 4V1h-1v3h1zm0 19v-3h-1v3h1zm8-8h3v-1h-3v1zM1 12H4v-1H1v1zm15.24-7.16l1.42 1.42 1.79-1.8-1.41-1.41-1.8 1.79zM4.22 18.36l1.42-1.42-1.8-1.79-1.41 1.41 1.79 1.8zM12 7a5 5 0 100 10 5 5 0 000-10z"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
1
frontend/public/icons/test.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M3 13h8v-2H3v2zm0-6h14V5H3v2zm0 12h18v-2H3v2z"/></svg>
|
||||
|
After Width: | Height: | Size: 144 B |
1
frontend/public/icons/user-plus.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M15 12c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM1 21v-2c0-2.76 5.82-4 9-4s9 1.24 9 4v2H1zM23 8h-2V6h-2V4h2V2h2v2h2v2h-2v2z"/></svg>
|
||||
|
After Width: | Height: | Size: 233 B |
@@ -1,73 +1,124 @@
|
||||
<template>
|
||||
<main style="font-family: system-ui, sans-serif; padding: 16px; max-width: 980px; margin: 0 auto;">
|
||||
<h1>RADIUS Dashboard</h1>
|
||||
<section style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px; align-items: start;">
|
||||
<div>
|
||||
<h2>Usuarios</h2>
|
||||
<form @submit.prevent="createUser" style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px;">
|
||||
<input v-model="form.username" placeholder="usuario" required />
|
||||
<input v-model="form.password" placeholder="contraseña" required />
|
||||
<input v-model="form.vlan" placeholder="VLAN" />
|
||||
<label><input type="checkbox" v-model="form.disabled" /> deshabilitado</label>
|
||||
<button type="submit">Crear / Actualizar</button>
|
||||
<header class="topbar">
|
||||
<div class="title">RADIUS Nucleo</div>
|
||||
<div class="actions">
|
||||
<button class="icon-btn" @click="toggleTheme">
|
||||
<img class="icon" :src="theme === 'light' ? '/icons/moon.svg' : '/icons/sun.svg'" alt="theme">
|
||||
</button>
|
||||
<span class="chip"><span class="muted">Estado:</span> {{ statusText }}</span>
|
||||
<button class="icon-btn" @click="openAddUser">
|
||||
<img class="icon" src="/icons/user-plus.svg" alt="usuario"> Usuario
|
||||
</button>
|
||||
<button class="icon-btn" @click="openAddGuest">
|
||||
<img class="icon" src="/icons/guest.svg" alt="invitado"> Invitado
|
||||
</button>
|
||||
<button class="icon-btn" @click="openSettings">
|
||||
<img class="icon" src="/icons/settings.svg" alt="config"> Configuración
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="shell">
|
||||
<!-- Sidebar: Eventos -->
|
||||
<aside class="panel" :class="{ collapsed: sidebarCollapsed }">
|
||||
<div class="panel-header">
|
||||
<div class="panel-title">Eventos FreeRADIUS</div>
|
||||
<div class="panel-actions">
|
||||
<button class="icon-btn" title="Filtrar" @click="showEventFilters = true"><img class="icon" src="/icons/filter.svg" alt="filtrar"></button>
|
||||
<button class="icon-btn" title="Limpiar" @click="clearRequests"><img class="icon" src="/icons/clear.svg" alt="limpiar"></button>
|
||||
<button class="icon-btn" title="Test" @click="selfTest"><img class="icon" src="/icons/test.svg" alt="test"></button>
|
||||
<a class="icon-btn" title="Descargar" href="/api/requests.csv" target="_blank"><img class="icon" src="/icons/download.svg" alt="descargar"></a>
|
||||
<button class="icon-btn" title="Copiar" @click="copyRequests"><img class="icon" src="/icons/copy.svg" alt="copiar"></button>
|
||||
<button class="icon-btn" title="Colapsar" @click="sidebarCollapsed = !sidebarCollapsed">{{ sidebarCollapsed ? 'Expandir' : 'Colapsar' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<div v-if="loading.requests" class="muted">Cargando eventos…</div>
|
||||
<EventCard v-for="ev in filteredRequests" :key="ev.id" :ev="ev" />
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main: Usuarios -->
|
||||
<main class="panel" :class="{ collapsed: mainCollapsed }">
|
||||
<div class="panel-header">
|
||||
<div class="row">
|
||||
<div class="panel-title">Usuarios y Dispositivos</div>
|
||||
<span class="spacer"></span>
|
||||
<button class="icon-btn" title="Vista usuarios" @click="layoutMode='user'">
|
||||
<img class="icon" src="/icons/layout-users.svg" alt="usuarios"/> Usuarios
|
||||
</button>
|
||||
<button class="icon-btn" title="Vista dispositivos" @click="layoutMode='device'">
|
||||
<img class="icon" src="/icons/layout-devices.svg" alt="dispositivos"/> Dispositivos
|
||||
</button>
|
||||
<button class="icon-btn" title="Filtrar" @click="showUserFilters = true"><img class="icon" src="/icons/filter.svg" alt="filtro"/></button>
|
||||
<button class="icon-btn" title="Colapsar" @click="mainCollapsed = !mainCollapsed">{{ mainCollapsed ? 'Expandir' : 'Colapsar' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<form @submit.prevent="createUser" class="row" style="margin-bottom:10px;">
|
||||
<input v-model="form.username" placeholder="usuario" required class="toggle"/>
|
||||
<input v-model="form.password" placeholder="contraseña" required class="toggle"/>
|
||||
<input v-model="form.vlan" placeholder="VLAN" class="toggle"/>
|
||||
<label class="row toggle" style="gap:6px;"><input type="checkbox" v-model="form.disabled"/> deshabilitado</label>
|
||||
<button type="submit" class="icon-btn">Crear / Actualizar</button>
|
||||
</form>
|
||||
<div v-if="loading.users">Cargando usuarios…</div>
|
||||
<table v-else style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:left">Usuario</th>
|
||||
<th style="text-align:left">VLAN</th>
|
||||
<th style="text-align:left">Estado</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="u in users" :key="u.username">
|
||||
<td>{{ u.username }}</td>
|
||||
<td>{{ u.vlan }}</td>
|
||||
<td>{{ u.disabled ? 'deshabilitado' : 'activo' }}</td>
|
||||
<td>
|
||||
<button @click="toggleDisable(u)">{{ u.disabled ? 'Habilitar' : 'Deshabilitar' }}</button>
|
||||
<button @click="removeUser(u)" style="margin-left: 6px">Eliminar</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Eventos</h2>
|
||||
<div style="margin-bottom: 8px; display:flex; gap:8px;">
|
||||
<button @click="refreshRequests">Refrescar</button>
|
||||
<button @click="clearRequests">Limpiar</button>
|
||||
<button @click="selfTest">Self test</button>
|
||||
<a :href="'/api/requests.csv'" target="_blank">Descargar CSV</a>
|
||||
</div>
|
||||
<div v-if="loading.requests">Cargando eventos…</div>
|
||||
<div v-else style="max-height: 420px; overflow: auto; border: 1px solid #ddd; padding: 8px;">
|
||||
<div v-for="ev in requests" :key="ev.id" style="border-bottom: 1px dashed #ddd; padding: 6px 0;">
|
||||
<div><b>{{ ev.ts }}</b> — {{ ev.type }}</div>
|
||||
<div v-if="ev.attrs" style="font-size: 12px; color: #444;">
|
||||
<span>User: {{ ev.attrs['User-Name'] || ev.attrs['User-Name*0'] }}</span>
|
||||
<span v-if="ev.attrs['NAS-IP-Address']"> — NAS: {{ ev.attrs['NAS-IP-Address'] }}</span>
|
||||
<span v-if="ev.attrs['Calling-Station-Id']"> — STA: {{ ev.attrs['Calling-Station-Id'] }}</span>
|
||||
</div>
|
||||
<div v-if="ev.decision">Decision: {{ ev.decision }}</div>
|
||||
<div v-if="ev.error" style="color: #a00;">Error: {{ ev.error }}</div>
|
||||
</div>
|
||||
<div v-if="loading.users" class="muted">Cargando usuarios…</div>
|
||||
<div v-else class="grid">
|
||||
<UserCard v-for="u in filteredUsers" :key="u.username" :item="u" :mode="layoutMode"
|
||||
@toggleDisable="toggleDisable" @remove="removeUser" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</main>
|
||||
</section>
|
||||
|
||||
<!-- Modals -->
|
||||
<Modal :open="showEventFilters" title="Filtros de eventos" @close="showEventFilters=false">
|
||||
<div class="row" style="margin:8px 0;">
|
||||
<input v-model="eventFilters.text" placeholder="Buscar texto" class="toggle" style="flex:1;"/>
|
||||
<select v-model="eventFilters.type" class="toggle">
|
||||
<option value="">Todos</option>
|
||||
<option value="authorize">authorize</option>
|
||||
<option value="accounting">accounting</option>
|
||||
<option value="post-auth">post-auth</option>
|
||||
<option value="selftest">selftest</option>
|
||||
<option value="coa-disconnect">coa-disconnect</option>
|
||||
</select>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal :open="showUserFilters" title="Filtros de usuarios" @close="showUserFilters=false">
|
||||
<div class="row" style="margin:8px 0;">
|
||||
<input v-model="userFilters.text" placeholder="Buscar usuario" class="toggle" style="flex:1;"/>
|
||||
<select v-model="userFilters.status" class="toggle">
|
||||
<option value="">Todos</option>
|
||||
<option value="active">Activos</option>
|
||||
<option value="disabled">Deshabilitados</option>
|
||||
</select>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { onMounted, reactive, ref, computed } from 'vue';
|
||||
import EventCard from './components/EventCard.js';
|
||||
import UserCard from './components/UserCard.js';
|
||||
import Modal from './components/Modal.vue';
|
||||
|
||||
const users = ref([]);
|
||||
const requests = ref([]);
|
||||
const loading = reactive({ users: false, requests: false });
|
||||
const form = reactive({ username: '', password: '', vlan: '', disabled: false });
|
||||
|
||||
const showEventFilters = ref(false);
|
||||
const showUserFilters = ref(false);
|
||||
const eventFilters = reactive({ text: '', type: '' });
|
||||
const userFilters = reactive({ text: '', status: '' });
|
||||
const sidebarCollapsed = ref(false);
|
||||
const mainCollapsed = ref(false);
|
||||
const layoutMode = ref('user');
|
||||
const theme = ref(localStorage.getItem('theme') || 'dark');
|
||||
const statusText = ref('OK');
|
||||
|
||||
async function fetchUsers() {
|
||||
loading.users = true;
|
||||
try {
|
||||
@@ -138,12 +189,45 @@ onMounted(async () => {
|
||||
await fetchUsers();
|
||||
await fetchRequests();
|
||||
setupSse();
|
||||
applyTheme();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; }
|
||||
table th, table td { padding: 4px 6px; border-bottom: 1px solid #eee; }
|
||||
button { padding: 6px 10px; cursor: pointer; }
|
||||
input { padding: 6px 8px; }
|
||||
</style>
|
||||
const filteredRequests = computed(() => {
|
||||
return requests.value.filter(ev => {
|
||||
if (eventFilters.type && ev.type !== eventFilters.type) return false;
|
||||
if (eventFilters.text) {
|
||||
const t = eventFilters.text.toLowerCase();
|
||||
const blob = JSON.stringify(ev).toLowerCase();
|
||||
if (!blob.includes(t)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
const filteredUsers = computed(() => {
|
||||
return users.value.filter(u => {
|
||||
if (userFilters.text && !u.username.toLowerCase().includes(userFilters.text.toLowerCase())) return false;
|
||||
if (userFilters.status === 'active' && u.disabled) return false;
|
||||
if (userFilters.status === 'disabled' && !u.disabled) return false;
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
function copyRequests() {
|
||||
const txt = JSON.stringify(requests.value, null, 2);
|
||||
navigator.clipboard?.writeText(txt);
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
theme.value = theme.value === 'light' ? 'dark' : 'light';
|
||||
localStorage.setItem('theme', theme.value);
|
||||
applyTheme();
|
||||
}
|
||||
function applyTheme() {
|
||||
document.documentElement.classList.toggle('light', theme.value === 'light');
|
||||
}
|
||||
|
||||
function openAddUser() { /* placeholder for advanced modal */ }
|
||||
function openAddGuest() { /* placeholder for advanced modal */ }
|
||||
function openSettings() { /* placeholder for advanced modal */ }
|
||||
</script>
|
||||
|
||||
27
frontend/src/components/EventCard.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { defineComponent, h } from 'vue';
|
||||
import htm from 'htm';
|
||||
const html = htm.bind(h);
|
||||
|
||||
export default defineComponent({
|
||||
name: 'EventCard',
|
||||
props: { ev: { type: Object, required: true } },
|
||||
setup(props) {
|
||||
return () => {
|
||||
const a = props.ev.attrs || {};
|
||||
return html`<div class="card">
|
||||
<div class="row">
|
||||
<b>${props.ev.type}</b>
|
||||
<span class="chip muted">${props.ev.ts}</span>
|
||||
${props.ev.decision ? html`<span class="chip">Decision: ${props.ev.decision}</span>` : ''}
|
||||
${props.ev.error ? html`<span class="chip" style="color:#b33">Error</span>` : ''}
|
||||
</div>
|
||||
<div class="muted" style="margin-top:6px; font-size:12px;">
|
||||
${a['User-Name'] || a['User-Name*0'] ? html`<span>User: ${a['User-Name'] || a['User-Name*0']}</span>` : ''}
|
||||
${a['NAS-IP-Address'] ? html`<span> — NAS: ${a['NAS-IP-Address']}</span>` : ''}
|
||||
${a['Calling-Station-Id'] ? html`<span> — STA: ${a['Calling-Station-Id']}</span>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
24
frontend/src/components/Modal.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div v-if="open" class="modal-backdrop" @click.self="$emit('close')">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<strong>{{ title }}</strong>
|
||||
<button class="icon-btn" @click="$emit('close')">Cerrar</button>
|
||||
</div>
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<slot name="footer">
|
||||
<button class="icon-btn" @click="$emit('close')">OK</button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({ open: Boolean, title: String });
|
||||
defineEmits(['close']);
|
||||
</script>
|
||||
|
||||
25
frontend/src/components/UserCard.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineComponent, h } from 'vue';
|
||||
import htm from 'htm';
|
||||
const html = htm.bind(h);
|
||||
|
||||
export default defineComponent({
|
||||
name: 'UserCard',
|
||||
props: { item: { type: Object, required: true }, mode: { type: String, default: 'user' } },
|
||||
emits: ['toggleDisable', 'remove'],
|
||||
setup(props, { emit }) {
|
||||
function toggle() { emit('toggleDisable', props.item); }
|
||||
function remove() { emit('remove', props.item); }
|
||||
return () => html`<div class="card">
|
||||
<div class="row">
|
||||
<b>${props.mode === 'user' ? props.item.username : (props.item.device || props.item.username)}</b>
|
||||
<span class="chip">VLAN ${props.item.vlan}</span>
|
||||
${props.item.disabled ? html`<span class="chip" style="color:#b33">deshabilitado</span>` : html`<span class="chip">activo</span>`}
|
||||
<span class="spacer"></span>
|
||||
<button class="icon-btn" onClick=${toggle}>${props.item.disabled ? 'Habilitar' : 'Deshabilitar'}</button>
|
||||
<button class="icon-btn" onClick=${remove}>Eliminar</button>
|
||||
</div>
|
||||
<div class="muted" style="margin-top:6px; font-size:12px;">${props.item.devices ? props.item.devices.length : 0} dispositivos</div>
|
||||
</div>`;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import './styles.css';
|
||||
|
||||
createApp(App).mount('#app');
|
||||
|
||||
const app = createApp(App);
|
||||
app.mount('#app');
|
||||
|
||||
75
frontend/src/styles.css
Normal file
@@ -0,0 +1,75 @@
|
||||
:root {
|
||||
--bg: 15 15 18;
|
||||
--fg: 235 235 240;
|
||||
--muted: 180 180 190;
|
||||
--accent: 80 160 255;
|
||||
--card: 28 28 34 / 0.55;
|
||||
--border: 255 255 255 / 0.12;
|
||||
--glass-blur: 14px;
|
||||
--radius: 14px;
|
||||
}
|
||||
:root.light {
|
||||
--bg: 245 245 248;
|
||||
--fg: 20 20 22;
|
||||
--muted: 110 110 120;
|
||||
--accent: 18 108 242;
|
||||
--card: 255 255 255 / 0.6;
|
||||
--border: 0 0 0 / 0.08;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body, #app { height: 100%; }
|
||||
html, body { margin: 0; padding: 0; background: rgb(var(--bg)); color: rgb(var(--fg)); }
|
||||
body { font: 14px/1.45 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; }
|
||||
button { cursor: pointer; }
|
||||
a { color: inherit; }
|
||||
|
||||
/* Top bar */
|
||||
.topbar {
|
||||
position: sticky; top: 0; z-index: 10;
|
||||
display: flex; flex-wrap: wrap; align-items: center;
|
||||
gap: 10px; padding: 10px 14px; backdrop-filter: blur(var(--glass-blur));
|
||||
background: linear-gradient(180deg, rgba(var(--card)), rgba(var(--card)) 60%, rgba(0,0,0,0));
|
||||
border-bottom: 1px solid rgba(var(--border));
|
||||
}
|
||||
.title { font-size: 16px; font-weight: 700; letter-spacing: .2px; flex: 1 1 auto; }
|
||||
.actions { display: inline-flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
||||
.icon-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 10px; border-radius: 10px; border: 1px solid rgba(var(--border)); background: rgba(var(--card)); color: inherit; transition: transform .12s ease, background .2s;
|
||||
backdrop-filter: blur(var(--glass-blur)); }
|
||||
.icon-btn:hover { transform: translateY(-1px); background: rgba(var(--card)); }
|
||||
.icon { width: 16px; height: 16px; opacity: .9; }
|
||||
|
||||
/* Layout */
|
||||
.shell { height: calc(100vh - 54px); display: grid; grid-template-columns: 360px 1fr; gap: 12px; padding: 12px; }
|
||||
.panel { border: 1px solid rgba(var(--border)); background: rgba(var(--card)); border-radius: var(--radius); backdrop-filter: blur(var(--glass-blur)); overflow: hidden; display: flex; flex-direction: column; min-height: 0; }
|
||||
.panel-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; padding: 10px 12px; border-bottom: 1px solid rgba(var(--border)); }
|
||||
.panel-title { font-weight: 600; }
|
||||
.panel-actions { display: inline-flex; flex-wrap: wrap; gap: 6px; }
|
||||
.scroll { overflow: auto; padding: 10px; }
|
||||
|
||||
/* Cards */
|
||||
.card { border: 1px solid rgba(var(--border)); background: rgba(var(--card)); border-radius: 12px; padding: 10px; transition: transform .12s ease, box-shadow .2s ease; box-shadow: 0 4px 14px rgba(0,0,0,.08); }
|
||||
.card:hover { transform: translateY(-1px); box-shadow: 0 8px 20px rgba(0,0,0,.12); }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 10px; }
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 980px) {
|
||||
.shell { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* Collapse helpers: keep headers visible when collapsed */
|
||||
.panel.collapsed .scroll { display: none; }
|
||||
|
||||
/* Modal */
|
||||
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.35); backdrop-filter: blur(4px); display: grid; place-items: center; z-index: 20; animation: fadeIn .15s ease;
|
||||
}
|
||||
.modal { width: min(680px, 92vw); border-radius: 14px; border: 1px solid rgba(var(--border)); background: rgba(var(--card)); padding: 14px; box-shadow: 0 10px 32px rgba(0,0,0,.2); }
|
||||
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
||||
.modal-footer { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; }
|
||||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||
|
||||
/* Small bits */
|
||||
.chip { display: inline-flex; align-items: center; gap: 6px; padding: 4px 8px; border: 1px solid rgba(var(--border)); border-radius: 999px; background: rgba(var(--card)); font-size: 12px; }
|
||||
.muted { color: rgb(var(--muted)); }
|
||||
.row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||||
.spacer { flex: 1; }
|
||||
.toggle { padding: 6px 10px; border-radius: 8px; border: 1px solid rgba(var(--border)); background: rgba(var(--card)); }
|
||||