initial commit
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/saas-builder.iml" filepath="$PROJECT_DIR$/.idea/saas-builder.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
9
.idea/saas-builder.iml
generated
Normal file
9
.idea/saas-builder.iml
generated
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
21
main.go
Normal file
21
main.go
Normal file
@ -0,0 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
//TIP <p>To run your code, right-click the code and select <b>Run</b>.</p> <p>Alternatively, click
|
||||
// the <icon src="AllIcons.Actions.Execute"/> icon in the gutter and select the <b>Run</b> menu item from here.</p>
|
||||
|
||||
func main() {
|
||||
//TIP <p>Press <shortcut actionId="ShowIntentionActions"/> when your caret is at the underlined text
|
||||
// to see how GoLand suggests fixing the warning.</p><p>Alternatively, if available, click the lightbulb to view possible fixes.</p>
|
||||
s := "gopher"
|
||||
fmt.Println("Hello and welcome, %s!", s)
|
||||
|
||||
for i := 1; i <= 5; i++ {
|
||||
//TIP <p>To start your debugging session, right-click your code in the editor and select the Debug option.</p> <p>We have set one <icon src="AllIcons.Debugger.Db_set_breakpoint"/> breakpoint
|
||||
// for you, but you can always add more by pressing <shortcut actionId="ToggleLineBreakpoint"/>.</p>
|
||||
fmt.Println("i =", 100/i)
|
||||
}
|
||||
}
|
98
readme.md
Normal file
98
readme.md
Normal file
@ -0,0 +1,98 @@
|
||||
# SAAS Builder
|
||||
|
||||
A simple framework or boilerplate that will provide everything you need in order to build a SAAS application.
|
||||
|
||||
Everything must be procedurally generated based on configuration.
|
||||
|
||||
Items required for a SAAS application:
|
||||
|
||||
* Web application
|
||||
* Mobile application
|
||||
* Marketing Website
|
||||
* Sign-up and payment
|
||||
* Authentication
|
||||
* backend business logic
|
||||
* CRUD
|
||||
* Specialized views and queries
|
||||
* Actions a user can take
|
||||
* Regularly scheduled tasks
|
||||
* Rule based events - trigger an action on a preset state (could supersede the scheduled tasks)
|
||||
* Data storage
|
||||
* Access rules
|
||||
* File/Object storage
|
||||
* Upload/Download
|
||||
* Access rules
|
||||
* Integration with external application specific tools and services for ingestion (e.g. Domo Workbench, Weave Sync App, Parentlink Data app, etc)
|
||||
* Notifications - send to frontend
|
||||
* Send messages via email, SMS, Slack, or other chat applications
|
||||
* Event streams - for internal and external consumption
|
||||
* Audit logging - for user actions
|
||||
* System logging and monitoring
|
||||
* System monitoring for uptime and maintenance
|
||||
* User feedback system
|
||||
* Support systems - chat, phone, email, ticketing
|
||||
* Sales - lead gen, demoing, closing, onboarding
|
||||
|
||||
Can everything be based on a single 'Entity' object that gets augmented with features when the config calls for it?
|
||||
|
||||
Let's run through some examples.
|
||||
|
||||
Some examples of entities are:
|
||||
* users
|
||||
* pages
|
||||
* blog articles
|
||||
* tasks
|
||||
* Notifications
|
||||
* Photos
|
||||
* Permissions
|
||||
* Customers
|
||||
* Addresses
|
||||
* Passwords
|
||||
* SMS Messages
|
||||
* SMS Threads
|
||||
* Databases
|
||||
* DB Tables
|
||||
* Charts
|
||||
* Preferences
|
||||
* Forms
|
||||
* Form Fields
|
||||
* User Actions
|
||||
* System Actions
|
||||
* Micro Services
|
||||
* Support Tickets
|
||||
* Templates
|
||||
|
||||
I'm trying to think through how I'd go about building a page with dynamic content on it. Let's keep it simple first. And build a basic blog.
|
||||
* Create an entity and set its name to be a 'page'.
|
||||
* Properties for the page will include a name
|
||||
* Features would include a template that renders blog articles with pagination
|
||||
* Child entities would include blog articles
|
||||
|
||||
I asked ChatGPT to come up with some example page types that I would need to implement and here is what it came up with:
|
||||
|
||||
* Login/Authentication Views: For user login, registration, password recovery, and multi-factor authentication.
|
||||
* Profile/User Account Views: For user settings, profiles, and account management.
|
||||
* Admin/Settings Views: For configuring application settings, roles, and permissions.
|
||||
* Settings/Preferences Views: For user preferences and application configurations.
|
||||
* Form Views: For data entry, including creating, editing, and submitting forms.
|
||||
* Wizard/Step-by-Step Views: For multi-step processes such as onboarding or checkout flows.
|
||||
* Wizard/Step-by-Step Views: For guiding users through a process with multiple steps.
|
||||
* Table/Grid Views: For displaying tabular data with sorting, filtering, and pagination.
|
||||
* Search/Filter Views: For advanced search functionalities with filtering options.
|
||||
* Notifications/Activity Feed Views: For displaying alerts, notifications, and activity logs.
|
||||
* Inbox/Message Views: For managing messages, emails, or chat interfaces.
|
||||
* Modal/Popup Views: For transient interactions like alerts, confirmations, and quick data entry.
|
||||
* Error/Empty State Views: For handling errors, no data, or empty states gracefully.
|
||||
* Calendar Views: For scheduling, booking, and time management features.
|
||||
* Charts/Graphs Views: For visualizing data with different types of charts (e.g., bar, line, pie).
|
||||
* Reports/Analytics Views: For detailed reports and analytics with drill-down capabilities.
|
||||
* Card Views: For summarizing information in a card format, useful for dashboards and overviews.
|
||||
* Map Views: For geographic data representation, such as location tracking or service areas.
|
||||
* Help/Support Views: For providing user assistance, FAQs, and support ticket submission.
|
||||
* Documentation Views: For displaying manuals, guides, and documentation.
|
||||
* Landing Page/Marketing Views: For public-facing marketing pages with promotional content.
|
||||
|
||||
Lower priority views
|
||||
* Kanban Board Views: For project and task management with drag-and-drop functionality.
|
||||
* Tree Views: For hierarchical data structures, such as file explorers or organizational charts.
|
||||
* Gallery Views: For displaying images or media content in a grid or masonry layout.
|
30
web/.gitignore
vendored
Normal file
30
web/.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
3
web/.vscode/extensions.json
vendored
Normal file
3
web/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
33
web/README.md
Normal file
33
web/README.md
Normal file
@ -0,0 +1,33 @@
|
||||
# web
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
1
web/env.d.ts
vendored
Normal file
1
web/env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
14
web/index.html
Normal file
14
web/index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="/src/output.css" rel="stylesheet">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
4441
web/package-lock.json
generated
Normal file
4441
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
web/package.json
Normal file
30
web/package.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build"
|
||||
},
|
||||
"dependencies": {
|
||||
"pinia": "^2.3.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "^22.0.0",
|
||||
"@types/node": "^22.10.2",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.6.3",
|
||||
"vite": "^6.0.5",
|
||||
"vite-plugin-vue-devtools": "^7.6.8",
|
||||
"vue-tsc": "^2.1.10"
|
||||
}
|
||||
}
|
BIN
web/public/favicon.ico
Normal file
BIN
web/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
128
web/src/App.vue
Normal file
128
web/src/App.vue
Normal file
@ -0,0 +1,128 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
import DynamicForm from './components/DynamicForm.vue';
|
||||
import { ref } from 'vue';
|
||||
import FormBuilder from "@/components/FormBuilder.vue";
|
||||
|
||||
// const formInputs = ref([
|
||||
// {
|
||||
// id: 'username',
|
||||
// label: 'Username',
|
||||
// type: 'text',
|
||||
// defaultValue: '',
|
||||
// required: true
|
||||
// },
|
||||
// {
|
||||
// id: 'email',
|
||||
// label: 'Email',
|
||||
// type: 'email',
|
||||
// defaultValue: '',
|
||||
// required: true
|
||||
// },
|
||||
// {
|
||||
// id: 'password',
|
||||
// label: 'Password',
|
||||
// type: 'password',
|
||||
// defaultValue: '',
|
||||
// required: true
|
||||
// },
|
||||
// {
|
||||
// id: 'bio',
|
||||
// label: 'Bio',
|
||||
// type: 'textarea',
|
||||
// defaultValue: '',
|
||||
// required: false
|
||||
// },
|
||||
// {
|
||||
// id: 'age',
|
||||
// label: 'Age',
|
||||
// type: 'number',
|
||||
// defaultValue: '18',
|
||||
// required: true,
|
||||
// min: '18',
|
||||
// max: '100'
|
||||
// },
|
||||
// {
|
||||
// id: 'favoriteColor',
|
||||
// label: 'Favorite Color',
|
||||
// type: 'color',
|
||||
// defaultValue: '#000000',
|
||||
// required: false
|
||||
// },
|
||||
// {
|
||||
// id: 'birthDate',
|
||||
// label: 'Birth Date',
|
||||
// type: 'date',
|
||||
// defaultValue: '',
|
||||
// required: true
|
||||
// },
|
||||
// {
|
||||
// id: 'appointmentTime',
|
||||
// label: 'Appointment Time',
|
||||
// type: 'time',
|
||||
// defaultValue: '',
|
||||
// required: true
|
||||
// },
|
||||
// {
|
||||
// id: 'category',
|
||||
// label: 'Category',
|
||||
// type: 'select',
|
||||
// required: true,
|
||||
// options: [
|
||||
// { label: 'Option 1', value: 'option1' },
|
||||
// { label: 'Option 2', value: 'option2' },
|
||||
// { label: 'Option 3', value: 'option3' }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: 'interests',
|
||||
// label: 'Interests',
|
||||
// type: 'checkbox',
|
||||
// required: false,
|
||||
// options: [
|
||||
// { label: 'Interest 1', value: 'interest1' },
|
||||
// { label: 'Interest 2', value: 'interest2' },
|
||||
// { label: 'Interest 3', value: 'interest3' }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: 'preferredContactMethod',
|
||||
// label: 'Preferred Contact Method',
|
||||
// type: 'radio',
|
||||
// required: true,
|
||||
// options: [
|
||||
// { label: 'Email', value: 'email' },
|
||||
// { label: 'Phone', value: 'phone' }
|
||||
// ]
|
||||
// },
|
||||
// {
|
||||
// id: 'tags',
|
||||
// label: 'Tags',
|
||||
// type: 'multi-input',
|
||||
// defaultValue: '',
|
||||
// required: false
|
||||
// }
|
||||
// ]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
|
||||
<div class="wrapper">
|
||||
<!-- <HelloWorld msg="You did it!" />-->
|
||||
<!-- <DynamicForm :formInputs="formInputs" />-->
|
||||
<FormBuilder />
|
||||
|
||||
<!-- <nav>-->
|
||||
<!-- <RouterLink to="/">Home</RouterLink>-->
|
||||
<!-- <RouterLink to="/about">About</RouterLink>-->
|
||||
<!-- </nav>-->
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- <RouterView />-->
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
87
web/src/assets/base.css
Normal file
87
web/src/assets/base.css
Normal file
@ -0,0 +1,87 @@
|
||||
/*!* color palette from <https://github.com/vuejs/theme> *!*/
|
||||
/*:root {*/
|
||||
/* --vt-c-white: #ffffff;*/
|
||||
/* --vt-c-white-soft: #f8f8f8;*/
|
||||
/* --vt-c-white-mute: #f2f2f2;*/
|
||||
|
||||
/* --vt-c-black: #181818;*/
|
||||
/* --vt-c-black-soft: #222222;*/
|
||||
/* --vt-c-black-mute: #282828;*/
|
||||
|
||||
/* --vt-c-indigo: #2c3e50;*/
|
||||
|
||||
/* --vt-c-divider-light-1: rgba(60, 60, 60, 0.29);*/
|
||||
/* --vt-c-divider-light-2: rgba(60, 60, 60, 0.12);*/
|
||||
/* --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);*/
|
||||
/* --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);*/
|
||||
|
||||
/* --vt-c-text-light-1: var(--vt-c-indigo);*/
|
||||
/* --vt-c-text-light-2: rgba(60, 60, 60, 0.66);*/
|
||||
/* --vt-c-text-dark-1: var(--vt-c-white);*/
|
||||
/* --vt-c-text-dark-2: rgba(235, 235, 235, 0.64);*/
|
||||
/*}*/
|
||||
|
||||
/*!* semantic color variables for this project *!*/
|
||||
/*:root {*/
|
||||
/* --color-background: var(--vt-c-white);*/
|
||||
/* --color-background-soft: var(--vt-c-white-soft);*/
|
||||
/* --color-background-mute: var(--vt-c-white-mute);*/
|
||||
|
||||
/* --color-border: var(--vt-c-divider-light-2);*/
|
||||
/* --color-border-hover: var(--vt-c-divider-light-1);*/
|
||||
|
||||
/* --color-heading: var(--vt-c-text-light-1);*/
|
||||
/* --color-text: var(--vt-c-text-light-1);*/
|
||||
|
||||
/* --section-gap: 160px;*/
|
||||
/*}*/
|
||||
|
||||
/*@media (prefers-color-scheme: dark) {*/
|
||||
/* :root {*/
|
||||
/* --color-background: var(--vt-c-black);*/
|
||||
/* --color-background-soft: var(--vt-c-black-soft);*/
|
||||
/* --color-background-mute: var(--vt-c-black-mute);*/
|
||||
|
||||
/* --color-border: var(--vt-c-divider-dark-2);*/
|
||||
/* --color-border-hover: var(--vt-c-divider-dark-1);*/
|
||||
|
||||
/* --color-heading: var(--vt-c-text-dark-1);*/
|
||||
/* --color-text: var(--vt-c-text-dark-2);*/
|
||||
/* }*/
|
||||
/*}*/
|
||||
|
||||
/**,*/
|
||||
/**::before,*/
|
||||
/**::after {*/
|
||||
/* box-sizing: border-box;*/
|
||||
/* margin: 0;*/
|
||||
/* font-weight: normal;*/
|
||||
/*}*/
|
||||
|
||||
/*body {*/
|
||||
/* min-height: 100vh;*/
|
||||
/* color: var(--color-text);*/
|
||||
/* background: var(--color-background);*/
|
||||
/* transition:*/
|
||||
/* color 0.5s,*/
|
||||
/* background-color 0.5s;*/
|
||||
/* line-height: 1.6;*/
|
||||
/* font-family:*/
|
||||
/* Inter,*/
|
||||
/* -apple-system,*/
|
||||
/* BlinkMacSystemFont,*/
|
||||
/* 'Segoe UI',*/
|
||||
/* Roboto,*/
|
||||
/* Oxygen,*/
|
||||
/* Ubuntu,*/
|
||||
/* Cantarell,*/
|
||||
/* 'Fira Sans',*/
|
||||
/* 'Droid Sans',*/
|
||||
/* 'Helvetica Neue',*/
|
||||
/* sans-serif;*/
|
||||
/* font-size: 15px;*/
|
||||
/* text-rendering: optimizeLegibility;*/
|
||||
/* -webkit-font-smoothing: antialiased;*/
|
||||
/* -moz-osx-font-smoothing: grayscale;*/
|
||||
/*}*/
|
||||
|
1
web/src/assets/logo.svg
Normal file
1
web/src/assets/logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
After Width: | Height: | Size: 276 B |
35
web/src/assets/main.css
Normal file
35
web/src/assets/main.css
Normal file
@ -0,0 +1,35 @@
|
||||
/*@import './base.css';*/
|
||||
|
||||
/*#app {*/
|
||||
/* max-width: 1280px;*/
|
||||
/* margin: 0 auto;*/
|
||||
/* padding: 2rem;*/
|
||||
/* font-weight: normal;*/
|
||||
/*}*/
|
||||
|
||||
/*a,*/
|
||||
/*.green {*/
|
||||
/* text-decoration: none;*/
|
||||
/* color: hsla(160, 100%, 37%, 1);*/
|
||||
/* transition: 0.4s;*/
|
||||
/* padding: 3px;*/
|
||||
/*}*/
|
||||
|
||||
/*@media (hover: hover) {*/
|
||||
/* a:hover {*/
|
||||
/* background-color: hsla(160, 100%, 37%, 0.2);*/
|
||||
/* }*/
|
||||
/*}*/
|
||||
|
||||
/*@media (min-width: 1024px) {*/
|
||||
/* body {*/
|
||||
/* display: flex;*/
|
||||
/* place-items: center;*/
|
||||
/* }*/
|
||||
|
||||
/* #app {*/
|
||||
/* display: grid;*/
|
||||
/* grid-template-columns: 1fr 1fr;*/
|
||||
/* padding: 0 2rem;*/
|
||||
/* }*/
|
||||
/*}*/
|
157
web/src/components/DynamicForm.vue
Normal file
157
web/src/components/DynamicForm.vue
Normal file
@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<form @submit.prevent="handleSubmit" class="space-y-2 p-6 bg-white shadow-lg rounded-lg max-w-md mx-auto">
|
||||
<div v-for="input in formInputs" :key="input.id" class="flex flex-col">
|
||||
<!-- if showLabel is undefined or set to true then show the label-->
|
||||
<label v-if="input.showLabel === undefined || input.showLabel" :for="input.id" class="mb-2 font-semibold text-gray-700">{{ input.label }}</label>
|
||||
|
||||
<!-- Text, Email, Password, Number -->
|
||||
<input
|
||||
v-if="['text', 'email', 'password', 'number', 'url', 'tel', 'search', 'color', 'date', 'datetime-local', 'month', 'time', 'week'].includes(input.type)"
|
||||
:id="input.id"
|
||||
:type="input.type"
|
||||
v-model="formData[input.id]"
|
||||
:required="input.required"
|
||||
:placeholder="input.placeholder"
|
||||
:min="input.min"
|
||||
:max="input.max"
|
||||
class="p-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
|
||||
<!-- Textarea -->
|
||||
<textarea
|
||||
v-if="input.type === 'textarea'"
|
||||
:id="input.id"
|
||||
v-model="formData[input.id]"
|
||||
:required="input.required"
|
||||
:min="input.min"
|
||||
:max="input.max"
|
||||
class="p-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
></textarea>
|
||||
|
||||
<!-- Select -->
|
||||
<select
|
||||
v-if="input.type === 'select'"
|
||||
:id="input.id"
|
||||
v-model="formData[input.id]"
|
||||
:required="input.required"
|
||||
class="p-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option v-for="option in input.options" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<!-- Radio -->
|
||||
<div v-if="input.type === 'radio'" class="space-y-2">
|
||||
<div v-for="option in input.options" :key="option.value" class="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
:id="`${input.id}-${option.value}`"
|
||||
:name="input.id"
|
||||
:value="option.value"
|
||||
v-model="formData[input.id]"
|
||||
:required="input.required"
|
||||
class="form-radio h-4 w-4 text-blue-600"
|
||||
/>
|
||||
<label :for="`${input.id}-${option.value}`" class="ml-2 text-gray-700">{{ option.label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Checkbox -->
|
||||
<div v-if="input.type === 'checkbox'" class="space-y-2">
|
||||
<div v-for="option in input.options" :key="option.value" class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
:id="`${input.id}-${option.value}`"
|
||||
:value="option.value"
|
||||
v-model="formData[input.id]"
|
||||
:required="input.required"
|
||||
class="form-checkbox h-4 w-4 text-blue-600"
|
||||
:checked="(!!input.defaultValue && input.defaultValue.includes(option.value))"
|
||||
/>
|
||||
<label :for="`${input.id}-${option.value}`" class="ml-2 text-gray-700">{{ option.label }}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Multi-input option -->
|
||||
<div v-if="input.type === 'multi-input'" class="space-y-2">
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
:id="`${input.id}-input`"
|
||||
v-model="multiInputValue"
|
||||
class="p-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 flex-1"
|
||||
/>
|
||||
<button type="button" @click="addMultiInputValue(input.id)" class="ml-2 px-3 py-2 bg-blue-600 text-white font-semibold rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<ul>
|
||||
<li v-for="(item, index) in formData[input.id]" :key="index" class="flex items-center space-x-2">
|
||||
<span>{{ item }}</span>
|
||||
<button type="button" @click="removeMultiInputValue(input.id, index)" class="text-red-600 hover:underline">Remove</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<button type="submit" class="px-6 py-3 bg-blue-600 text-white font-semibold rounded-md shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, defineEmits } from 'vue';
|
||||
|
||||
export interface FormInput {
|
||||
id: string;
|
||||
label: string;
|
||||
type: string;
|
||||
defaultValue?: string | string[];
|
||||
placeholder?: string;
|
||||
required: boolean;
|
||||
showLabel?: boolean;
|
||||
min?: string;
|
||||
max?: string;
|
||||
options?: { label: string, value: string }[]; // For select, radio, and checkbox types
|
||||
}
|
||||
|
||||
const props = defineProps<{ formInputs: FormInput[] }>();
|
||||
const emit = defineEmits(['send'])
|
||||
|
||||
const formData = reactive<Record<string, any>>({});
|
||||
|
||||
// Initialize formData with default values
|
||||
props.formInputs.forEach(input => {
|
||||
if (input.type === 'checkbox' || input.type === 'multi-input') {
|
||||
// if input.defaultValue is an array, use it,
|
||||
// otherwise create an empty array and
|
||||
// if input.defaultValue is a value that matches an option value, insert it into the array
|
||||
formData[input.id] = Array.isArray(input.defaultValue) ? input.defaultValue : (input.defaultValue ? [input.defaultValue] : []);
|
||||
} else {
|
||||
formData[input.id] = input.defaultValue || '';
|
||||
}
|
||||
});
|
||||
|
||||
const multiInputValue = ref<string>('');
|
||||
|
||||
const handleSubmit = () => {
|
||||
console.log('Form Data:', formData);
|
||||
emit('send', formData);
|
||||
};
|
||||
|
||||
const addMultiInputValue = (id: string) => {
|
||||
if (multiInputValue.value.trim()) {
|
||||
formData[id].push(multiInputValue.value.trim());
|
||||
multiInputValue.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const removeMultiInputValue = (id: string, index: number) => {
|
||||
formData[id].splice(index, 1);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Add any additional styling if needed */
|
||||
</style>
|
142
web/src/components/FormBuilder.vue
Normal file
142
web/src/components/FormBuilder.vue
Normal file
@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-100 flex flex-row items-start p-4">
|
||||
<div class="flex-col flex-1 items-center justify-center p-4">
|
||||
<h2 class="text-xl font-bold mb-4">Form Builder</h2>
|
||||
<DynamicForm :formInputs="formBuilderInputs" @send="handleFormBuilderSubmit" />
|
||||
</div>
|
||||
|
||||
<div v-if="generatedFormInputs.length" class="flex-col flex-1 items-center justify-center p-4">
|
||||
<h2 class="text-xl font-bold mb-4">Generated Form</h2>
|
||||
<DynamicForm :formInputs="generatedFormInputs" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import DynamicForm from './DynamicForm.vue';
|
||||
import type {FormInput} from "@/components/DynamicForm.vue";
|
||||
|
||||
const formBuilderInputs = ref<FormInput[]>([
|
||||
{
|
||||
id: 'fieldType',
|
||||
label: 'Field Type',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ label: 'Text', value: 'text' },
|
||||
{ label: 'Email', value: 'email' },
|
||||
{ label: 'Password', value: 'password' },
|
||||
{ label: 'Number', value: 'number' },
|
||||
{ label: 'Textarea', value: 'textarea' },
|
||||
{ label: 'Select', value: 'select' },
|
||||
{ label: 'Radio', value: 'radio' },
|
||||
{ label: 'Checkbox', value: 'checkbox' },
|
||||
{ label: 'Multi-Input', value: 'multi-input' },
|
||||
{ label: 'URL', value: 'url' },
|
||||
{ label: 'Telephone', value: 'tel' },
|
||||
{ label: 'Search', value: 'search' },
|
||||
{ label: 'Color', value: 'color' },
|
||||
{ label: 'Date', value: 'date' },
|
||||
{ label: 'Datetime-local', value: 'datetime-local' },
|
||||
{ label: 'Month', value: 'month' },
|
||||
{ label: 'Time', value: 'time' },
|
||||
{ label: 'Week', value: 'week' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'fieldLabel',
|
||||
label: 'Field Label',
|
||||
type: 'text',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
id: 'fieldID',
|
||||
label: 'Field ID',
|
||||
type: 'text',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
id: 'placeholder',
|
||||
label: 'Placeholder',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
id: 'required',
|
||||
label: 'Required',
|
||||
type: 'checkbox',
|
||||
required: false,
|
||||
showLabel: false,
|
||||
options: [{ label: 'Required', value: 'true' }]
|
||||
},
|
||||
{
|
||||
id: 'showLabel',
|
||||
label: 'Show Label',
|
||||
type: 'checkbox',
|
||||
required: false,
|
||||
showLabel: false,
|
||||
defaultValue: ['true'],
|
||||
options: [{ label: 'Show Label', value: 'true' }]
|
||||
},
|
||||
{
|
||||
id: 'defaultValue',
|
||||
label: 'Default Value',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
id: 'min',
|
||||
label: 'Min (for Number, Date, etc.)',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
id: 'max',
|
||||
label: 'Max (for Number, Date, etc.)',
|
||||
type: 'text',
|
||||
required: false
|
||||
},
|
||||
{
|
||||
id: 'options',
|
||||
label: 'Options (for Select, Radio, Checkbox)',
|
||||
type: 'multi-input',
|
||||
required: false
|
||||
}
|
||||
]);
|
||||
|
||||
const generatedFormInputs = ref<FormInput[]>([]);
|
||||
|
||||
const handleFormBuilderSubmit = (formData: Record<string, any>) => {
|
||||
const newField: FormInput = {
|
||||
id: formData.fieldID,
|
||||
label: formData.fieldLabel,
|
||||
type: formData.fieldType,
|
||||
placeholder: formData.placeholder || undefined,
|
||||
required: formData.required && formData.required.includes('true'),
|
||||
showLabel: formData.showLabel && formData.showLabel.includes('true'),
|
||||
defaultValue: formData.defaultValue || undefined,
|
||||
min: formData.min || undefined,
|
||||
max: formData.max || undefined,
|
||||
options: formData.options ? formData.options.map((opt: string) => ({ label: opt, value: opt })) : undefined
|
||||
};
|
||||
|
||||
generatedFormInputs.value.push(newField);
|
||||
|
||||
// Reset form builder fields
|
||||
formBuilderInputs.value.forEach(input => {
|
||||
if (input.type === 'checkbox' || input.type === 'multi-input') {
|
||||
formData[input.id] = [];
|
||||
if (input.id === 'showLabel') { // showLabel should default to true
|
||||
formData[input.id] = ['true'];
|
||||
}
|
||||
} else {
|
||||
formData[input.id] = '';
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Add any additional styling if needed */
|
||||
</style>
|
41
web/src/components/HelloWorld.vue
Normal file
41
web/src/components/HelloWorld.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
msg: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="greetings">
|
||||
<h1 class="green">{{ msg }}</h1>
|
||||
<h3>
|
||||
You’ve successfully created a project with
|
||||
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
font-weight: 500;
|
||||
font-size: 2.6rem;
|
||||
position: relative;
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
94
web/src/components/TheWelcome.vue
Normal file
94
web/src/components/TheWelcome.vue
Normal file
@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import WelcomeItem from './WelcomeItem.vue'
|
||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
||||
import ToolingIcon from './icons/IconTooling.vue'
|
||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
||||
import CommunityIcon from './icons/IconCommunity.vue'
|
||||
import SupportIcon from './icons/IconSupport.vue'
|
||||
|
||||
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<DocumentationIcon />
|
||||
</template>
|
||||
<template #heading>Documentation</template>
|
||||
|
||||
Vue’s
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
||||
provides you with all information you need to get started.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<ToolingIcon />
|
||||
</template>
|
||||
<template #heading>Tooling</template>
|
||||
|
||||
This project is served and bundled with
|
||||
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
||||
recommended IDE setup is
|
||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
|
||||
+
|
||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
||||
you need to test your components and web pages, check out
|
||||
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vite</a>
|
||||
and
|
||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
|
||||
/
|
||||
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
|
||||
|
||||
<br />
|
||||
|
||||
More instructions are available in
|
||||
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
|
||||
>.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<EcosystemIcon />
|
||||
</template>
|
||||
<template #heading>Ecosystem</template>
|
||||
|
||||
Get official tools and libraries for your project:
|
||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
||||
you need more resources, we suggest paying
|
||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
||||
a visit.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<CommunityIcon />
|
||||
</template>
|
||||
<template #heading>Community</template>
|
||||
|
||||
Got stuck? Ask your question on
|
||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
|
||||
(our official Discord server), or
|
||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
||||
>StackOverflow</a
|
||||
>. You should also follow the official
|
||||
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
|
||||
Bluesky account or the
|
||||
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
||||
X account for latest news in the Vue world.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<SupportIcon />
|
||||
</template>
|
||||
<template #heading>Support Vue</template>
|
||||
|
||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
||||
us by
|
||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
||||
</WelcomeItem>
|
||||
</template>
|
87
web/src/components/WelcomeItem.vue
Normal file
87
web/src/components/WelcomeItem.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="item">
|
||||
<i>
|
||||
<slot name="icon"></slot>
|
||||
</i>
|
||||
<div class="details">
|
||||
<h3>
|
||||
<slot name="heading"></slot>
|
||||
</h3>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.details {
|
||||
flex: 1;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
i {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.4rem;
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.item {
|
||||
margin-top: 0;
|
||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
i {
|
||||
top: calc(50% - 25px);
|
||||
left: -26px;
|
||||
position: absolute;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.item:before {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:after {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:first-of-type:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.item:last-of-type:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
7
web/src/components/icons/IconCommunity.vue
Normal file
7
web/src/components/icons/IconCommunity.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
7
web/src/components/icons/IconDocumentation.vue
Normal file
7
web/src/components/icons/IconDocumentation.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||
<path
|
||||
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
7
web/src/components/icons/IconEcosystem.vue
Normal file
7
web/src/components/icons/IconEcosystem.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
7
web/src/components/icons/IconSupport.vue
Normal file
7
web/src/components/icons/IconSupport.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
19
web/src/components/icons/IconTooling.vue
Normal file
19
web/src/components/icons/IconTooling.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--mdi"
|
||||
width="24"
|
||||
height="24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
3
web/src/input.css
Normal file
3
web/src/input.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
14
web/src/main.ts
Normal file
14
web/src/main.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
805
web/src/output.css
Normal file
805
web/src/output.css
Normal file
@ -0,0 +1,805 @@
|
||||
*, ::before, ::after {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
--tw-contain-size: ;
|
||||
--tw-contain-layout: ;
|
||||
--tw-contain-paint: ;
|
||||
--tw-contain-style: ;
|
||||
}
|
||||
|
||||
::backdrop {
|
||||
--tw-border-spacing-x: 0;
|
||||
--tw-border-spacing-y: 0;
|
||||
--tw-translate-x: 0;
|
||||
--tw-translate-y: 0;
|
||||
--tw-rotate: 0;
|
||||
--tw-skew-x: 0;
|
||||
--tw-skew-y: 0;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
--tw-pan-x: ;
|
||||
--tw-pan-y: ;
|
||||
--tw-pinch-zoom: ;
|
||||
--tw-scroll-snap-strictness: proximity;
|
||||
--tw-gradient-from-position: ;
|
||||
--tw-gradient-via-position: ;
|
||||
--tw-gradient-to-position: ;
|
||||
--tw-ordinal: ;
|
||||
--tw-slashed-zero: ;
|
||||
--tw-numeric-figure: ;
|
||||
--tw-numeric-spacing: ;
|
||||
--tw-numeric-fraction: ;
|
||||
--tw-ring-inset: ;
|
||||
--tw-ring-offset-width: 0px;
|
||||
--tw-ring-offset-color: #fff;
|
||||
--tw-ring-color: rgb(59 130 246 / 0.5);
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
--tw-shadow: 0 0 #0000;
|
||||
--tw-shadow-colored: 0 0 #0000;
|
||||
--tw-blur: ;
|
||||
--tw-brightness: ;
|
||||
--tw-contrast: ;
|
||||
--tw-grayscale: ;
|
||||
--tw-hue-rotate: ;
|
||||
--tw-invert: ;
|
||||
--tw-saturate: ;
|
||||
--tw-sepia: ;
|
||||
--tw-drop-shadow: ;
|
||||
--tw-backdrop-blur: ;
|
||||
--tw-backdrop-brightness: ;
|
||||
--tw-backdrop-contrast: ;
|
||||
--tw-backdrop-grayscale: ;
|
||||
--tw-backdrop-hue-rotate: ;
|
||||
--tw-backdrop-invert: ;
|
||||
--tw-backdrop-opacity: ;
|
||||
--tw-backdrop-saturate: ;
|
||||
--tw-backdrop-sepia: ;
|
||||
--tw-contain-size: ;
|
||||
--tw-contain-layout: ;
|
||||
--tw-contain-paint: ;
|
||||
--tw-contain-style: ;
|
||||
}
|
||||
|
||||
/*
|
||||
! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com
|
||||
*/
|
||||
|
||||
/*
|
||||
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
||||
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
||||
*/
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
/* 1 */
|
||||
border-width: 0;
|
||||
/* 2 */
|
||||
border-style: solid;
|
||||
/* 2 */
|
||||
border-color: #e5e7eb;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
::before,
|
||||
::after {
|
||||
--tw-content: '';
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use a consistent sensible line-height in all browsers.
|
||||
2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
3. Use a more readable tab size.
|
||||
4. Use the user's configured `sans` font-family by default.
|
||||
5. Use the user's configured `sans` font-feature-settings by default.
|
||||
6. Use the user's configured `sans` font-variation-settings by default.
|
||||
7. Disable tap highlights on iOS
|
||||
*/
|
||||
|
||||
html,
|
||||
:host {
|
||||
line-height: 1.5;
|
||||
/* 1 */
|
||||
-webkit-text-size-adjust: 100%;
|
||||
/* 2 */
|
||||
-moz-tab-size: 4;
|
||||
/* 3 */
|
||||
-o-tab-size: 4;
|
||||
tab-size: 4;
|
||||
/* 3 */
|
||||
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
/* 4 */
|
||||
font-feature-settings: normal;
|
||||
/* 5 */
|
||||
font-variation-settings: normal;
|
||||
/* 6 */
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
/* 7 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove the margin in all browsers.
|
||||
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Add the correct height in Firefox.
|
||||
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
|
||||
3. Ensure horizontal rules are visible by default.
|
||||
*/
|
||||
|
||||
hr {
|
||||
height: 0;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 2 */
|
||||
border-top-width: 1px;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct text decoration in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
abbr:where([title]) {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the default font size and weight for headings.
|
||||
*/
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset links to optimize for opt-in styling instead of opt-out.
|
||||
*/
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font weight in Edge and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Use the user's configured `mono` font-family by default.
|
||||
2. Use the user's configured `mono` font-feature-settings by default.
|
||||
3. Use the user's configured `mono` font-variation-settings by default.
|
||||
4. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
/* 1 */
|
||||
font-feature-settings: normal;
|
||||
/* 2 */
|
||||
font-variation-settings: normal;
|
||||
/* 3 */
|
||||
font-size: 1em;
|
||||
/* 4 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
|
||||
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||
3. Remove gaps between table borders by default.
|
||||
*/
|
||||
|
||||
table {
|
||||
text-indent: 0;
|
||||
/* 1 */
|
||||
border-color: inherit;
|
||||
/* 2 */
|
||||
border-collapse: collapse;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
1. Change the font styles in all browsers.
|
||||
2. Remove the margin in Firefox and Safari.
|
||||
3. Remove default padding in all browsers.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
/* 1 */
|
||||
font-feature-settings: inherit;
|
||||
/* 1 */
|
||||
font-variation-settings: inherit;
|
||||
/* 1 */
|
||||
font-size: 100%;
|
||||
/* 1 */
|
||||
font-weight: inherit;
|
||||
/* 1 */
|
||||
line-height: inherit;
|
||||
/* 1 */
|
||||
letter-spacing: inherit;
|
||||
/* 1 */
|
||||
color: inherit;
|
||||
/* 1 */
|
||||
margin: 0;
|
||||
/* 2 */
|
||||
padding: 0;
|
||||
/* 3 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inheritance of text transform in Edge and Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Remove default button styles.
|
||||
*/
|
||||
|
||||
button,
|
||||
input:where([type='button']),
|
||||
input:where([type='reset']),
|
||||
input:where([type='submit']) {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
background-color: transparent;
|
||||
/* 2 */
|
||||
background-image: none;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Use the modern Firefox focus style for all focusable elements.
|
||||
*/
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
|
||||
*/
|
||||
|
||||
:-moz-ui-invalid {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct vertical alignment in Chrome and Firefox.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/*
|
||||
Correct the cursor style of increment and decrement buttons in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the odd appearance in Chrome and Safari.
|
||||
2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type='search'] {
|
||||
-webkit-appearance: textfield;
|
||||
/* 1 */
|
||||
outline-offset: -2px;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button;
|
||||
/* 1 */
|
||||
font: inherit;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct display in Chrome and Safari.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/*
|
||||
Removes the default spacing and border for appropriate elements.
|
||||
*/
|
||||
|
||||
blockquote,
|
||||
dl,
|
||||
dd,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
hr,
|
||||
figure,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
menu {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Reset default styling for dialogs.
|
||||
*/
|
||||
|
||||
dialog {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
Prevent resizing textareas horizontally by default.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
|
||||
2. Set the default placeholder color to the user's configured gray 400 color.
|
||||
*/
|
||||
|
||||
input::-moz-placeholder, textarea::-moz-placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
opacity: 1;
|
||||
/* 1 */
|
||||
color: #9ca3af;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Set the default cursor for buttons.
|
||||
*/
|
||||
|
||||
button,
|
||||
[role="button"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*
|
||||
Make sure disabled buttons don't get the pointer cursor.
|
||||
*/
|
||||
|
||||
:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/*
|
||||
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
|
||||
This can trigger a poorly considered lint error in some tools but is included by design.
|
||||
*/
|
||||
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
canvas,
|
||||
audio,
|
||||
iframe,
|
||||
embed,
|
||||
object {
|
||||
display: block;
|
||||
/* 1 */
|
||||
vertical-align: middle;
|
||||
/* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
|
||||
*/
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Make elements with the HTML hidden attribute stay hidden by default */
|
||||
|
||||
[hidden]:where(:not([hidden="until-found"])) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.absolute {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.mt-12 {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.h-4 {
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.w-4 {
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.max-w-md {
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
.flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.items-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.space-y-6 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
|
||||
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
||||
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.space-y-2 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse)));
|
||||
margin-bottom: calc(0.5rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.rounded-md {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.border-gray-300 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
|
||||
}
|
||||
|
||||
.bg-blue-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1));
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
|
||||
}
|
||||
|
||||
.bg-gray-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
|
||||
}
|
||||
|
||||
.p-3 {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.px-6 {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.py-3 {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.px-3 {
|
||||
padding-left: 0.75rem;
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
|
||||
.py-2 {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
.text-xl {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.text-gray-700 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(55 65 81 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.text-white {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.text-blue-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(37 99 235 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.text-red-600 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(220 38 38 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.shadow-lg {
|
||||
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.shadow-md {
|
||||
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||
}
|
||||
|
||||
.hover\:bg-blue-700:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1));
|
||||
}
|
||||
|
||||
.hover\:underline:hover {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
.focus\:outline-none:focus {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.focus\:ring-2:focus {
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||
}
|
||||
|
||||
.focus\:ring-blue-500:focus {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity, 1));
|
||||
}
|
23
web/src/router/index.ts
Normal file
23
web/src/router/index.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
// route level code-splitting
|
||||
// this generates a separate chunk (About.[hash].js) for this route
|
||||
// which is lazy-loaded when the route is visited.
|
||||
component: () => import('../views/AboutView.vue'),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export default router
|
12
web/src/stores/counter.ts
Normal file
12
web/src/stores/counter.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
15
web/src/views/AboutView.vue
Normal file
15
web/src/views/AboutView.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@media (min-width: 1024px) {
|
||||
.about {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
9
web/src/views/HomeView.vue
Normal file
9
web/src/views/HomeView.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import TheWelcome from '../components/TheWelcome.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<TheWelcome />
|
||||
</main>
|
||||
</template>
|
9
web/tailwind.config.js
Normal file
9
web/tailwind.config.js
Normal file
@ -0,0 +1,9 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./src/**/*.{html,js,vue}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
12
web/tsconfig.app.json
Normal file
12
web/tsconfig.app.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
11
web/tsconfig.json
Normal file
11
web/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
18
web/tsconfig.node.json
Normal file
18
web/tsconfig.node.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
18
web/vite.config.ts
Normal file
18
web/vite.config.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
})
|
Reference in New Issue
Block a user