Compare commits

..

No commits in common. "3a8e601a3793cdd484aa4f7ee64ccd92704ab9aa" and "a030a84652805323536da4038c017d60019a81b3" have entirely different histories.

35 changed files with 1035 additions and 1187 deletions

View File

@ -60,7 +60,6 @@ jobs:
build-args: |
APP_ENV=${{ env.VERSION == 'dev' && 'local' || 'production' }}
COMPOSER_FLAGS=${{ env.VERSION == 'dev' && '--optimize-autoloader --no-interaction' || '--no-dev --optimize-autoloader --no-interaction' }}
APP_VERSION=${{ env.VERSION }}
tags: ${{ env.API_TAGS }}
cache-from: type=registry,ref=${{secrets.DOCKER_API_REPO}}:dev
cache-to: type=inline
@ -72,8 +71,6 @@ jobs:
file: docker/Dockerfile.client
platforms: linux/amd64,linux/arm64
push: true
build-args: |
APP_VERSION=${{ env.VERSION }}
tags: ${{ env.UI_TAGS }}
cache-from: type=registry,ref=${{secrets.DOCKER_UI_REPO}}:dev
cache-to: type=inline

View File

@ -1,131 +0,0 @@
# OpnForm Nginx Setup Guide
This guide explains how to set up OpnForm with a host-level nginx configuration.
## Architecture Overview
The modified setup removes the main nginx ingress container and exposes services directly:
- **UI Service**: Exposed on port 7655 (HTTP)
- **API Service**: Exposed on port 7654 (HTTP via minimal nginx container)
- **Database**: PostgreSQL (internal only)
- **Redis**: Cache service (internal only)
## Key Changes from Default Setup
1. **Removed YAML anchors** - Each container now has its own explicit configuration to avoid conflicts
2. **Removed main ingress container** - Your host nginx handles all routing
3. **Added minimal api-nginx** - Small nginx container just to convert FastCGI to HTTP for the API
4. **Custom ports** - Using 7654-7655 range to avoid conflicts
## Setup Steps
### 1. Stop any existing containers
```bash
docker compose down
docker compose -f docker-compose.dev.yml down
```
### 2. Run the setup script
```bash
./scripts/docker-setup.sh
```
### 3. Verify services are running
```bash
docker compose ps
```
You should see:
- opnform-api (healthy)
- opnform-api-nginx (healthy)
- opnform-api-worker (running)
- opnform-api-scheduler (running)
- opnform-client (healthy)
- opnform-redis (healthy)
- opnform-db (healthy)
### 4. Configure your host nginx
Copy the example configuration:
```bash
sudo cp nginx-host-example.conf /etc/nginx/sites-available/forms.portnimara.dev
sudo ln -s /etc/nginx/sites-available/forms.portnimara.dev /etc/nginx/sites-enabled/
```
Edit the file to adjust:
- SSL certificate paths
- Server name if different
- Any other site-specific settings
### 5. Test nginx configuration
```bash
sudo nginx -t
```
### 6. Reload nginx
```bash
sudo systemctl reload nginx
```
## Troubleshooting
### Port already in use
If you get "port already allocated" errors:
1. Check what's using the ports:
```bash
sudo lsof -i :7654
sudo lsof -i :7655
```
2. Stop conflicting services or change the ports in docker-compose.yml
### API not responding
1. Check the api-nginx logs:
```bash
docker logs opnform-api-nginx
```
2. Verify the API container is running:
```bash
docker logs opnform-api
```
### UI not loading
1. Check the client logs:
```bash
docker logs opnform-client
```
2. Ensure the client/.env file has correct API URL settings
## Port Reference
- **7654**: API (HTTP) - proxied through api-nginx to PHP-FPM
- **7655**: UI (HTTP) - Nuxt.js frontend
- **9000**: PHP-FPM (internal only, FastCGI protocol)
- **5432**: PostgreSQL (internal only)
- **6379**: Redis (internal only)
## Security Notes
1. Ports are bound to 127.0.0.1 only, not exposed to external network
2. All traffic should go through your host nginx with SSL
3. The minimal api-nginx container only handles FastCGI conversion, no SSL termination
## Default Credentials
- Email: admin@opnform.com
- Password: password
**Important**: Change these immediately after first login!

View File

@ -14,7 +14,6 @@ class FeatureFlagsController extends Controller
'self_hosted' => config('app.self_hosted', true),
'custom_domains' => config('custom-domains.enabled', false),
'ai_features' => !empty(config('services.openai.api_key')),
'version' => $this->getAppVersion(),
'billing' => [
'enabled' => !empty(config('cashier.key')) && !empty(config('cashier.secret')),
@ -45,17 +44,4 @@ class FeatureFlagsController extends Controller
return response()->json($featureFlags);
}
/**
* Get the application version from Docker environment or fallback
*/
private function getAppVersion(): ?string
{
// Only return version for self-hosted installations
if (!config('app.self_hosted', true)) {
return null;
}
return config('app.docker_version');
}
}

View File

@ -179,9 +179,9 @@ class StoreFormSubmissionJob implements ShouldQueue
}
} else {
// Standard field processing (text, ID generation, etc.)
if ((!$answerValue || !Str::isUuid($answerValue)) && $field['type'] == 'text' && isset($field['generates_uuid']) && $field['generates_uuid']) {
if ($field['type'] == 'text' && isset($field['generates_uuid']) && $field['generates_uuid']) {
$finalData[$field['id']] = ($this->form->is_pro) ? Str::uuid()->toString() : 'Please upgrade your OpenForm subscription to use our ID generation features';
} elseif ((!$answerValue || !is_int($answerValue)) && $field['type'] == 'text' && isset($field['generates_auto_increment_id']) && $field['generates_auto_increment_id']) {
} elseif ($field['type'] == 'text' && isset($field['generates_auto_increment_id']) && $field['generates_auto_increment_id']) {
$finalData[$field['id']] = ($this->form->is_pro) ? (string) ($this->form->submissions_count + 1) : 'Please upgrade your OpenForm subscription to use our ID generation features';
} else {
$finalData[$field['id']] = $answerValue;

View File

@ -47,14 +47,6 @@ class IntegrationLogicRule implements DataAwareRule, ValidationRule
return;
}
$typeField = $condition['value']['property_meta']['type'];
$operator = $condition['value']['operator'];
// If operator has no format and no expected_type, it means it doesn't need input
if (!isset(FormPropertyLogicRule::getConditionMapping()[$typeField]['comparators'][$operator]['expected_type'])) {
return;
}
if (!isset($condition['value']['value'])) {
$this->isConditionCorrect = false;
$this->conditionErrors[] = 'missing condition value';
@ -62,6 +54,8 @@ class IntegrationLogicRule implements DataAwareRule, ValidationRule
return;
}
$typeField = $condition['value']['property_meta']['type'];
$operator = $condition['value']['operator'];
$value = $condition['value']['value'];
if (!isset(FormPropertyLogicRule::getConditionMapping()[$typeField])) {

View File

@ -15,17 +15,6 @@ return [
'name' => env('APP_NAME', 'OpnForm'),
/*
|--------------------------------------------------------------------------
| Application Version
|--------------------------------------------------------------------------
|
| This value is the version of your application. Used for display purposes
| and fallback when Docker build version is not available.
|
*/
'docker_version' => env('APP_VERSION_DOCKER'),
/*
|--------------------------------------------------------------------------
| Application Environment

View File

@ -45,58 +45,3 @@ it('can CRUD form integration', function () {
'message' => 'Form Integration was deleted.'
]);
});
it('can create form integration with checkbox logic', function () {
$user = $this->actingAsProUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'properties' => [
[
'id' => 'checkbox_field',
'name' => 'Checkbox Field',
'type' => 'checkbox'
],
[
'id' => 'text_field',
'name' => 'Text Field',
'type' => 'text',
],
],
]);
$data = [
'status' => true,
'integration_id' => 'email',
'logic' => [
'operatorIdentifier' => 'and',
'children' => [
[
'identifier' => 'checkbox_field',
'value' => [
'operator' => 'is_checked',
'property_meta' => [
'id' => 'checkbox_field',
'type' => 'checkbox',
]
],
],
],
],
'settings' => [
'send_to' => 'test@test.com',
'sender_name' => 'OpnForm',
'subject' => 'New form submission with checkbox logic',
'email_content' => 'Checkbox logic triggered.',
'include_submission_data' => true,
'include_hidden_fields_submission_data' => false,
'reply_to' => null
]
];
$this->postJson(route('open.forms.integration.create', $form->id), $data)
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form Integration was created.'
]);
});

View File

@ -1,6 +1,6 @@
<?php
use Illuminate\Support\Str;
it('can update form with existing record', function () {
$user = $this->actingAsProUser();
@ -39,56 +39,3 @@ it('can update form with existing record', function () {
expect($response->json('data.' . $nameProperty['id']))->toBe('Testing Updated');
}
});
it('can update form with existing record but generates_uuid field is not update', function () {
$user = $this->actingAsProUser();
$workspace = $this->createUserWorkspace($user);
$form = $this->createForm($user, $workspace, [
'editable_submissions' => true,
'properties' => [
[
'id' => 'uuid_field',
'type' => 'text',
'generates_uuid' => true,
'name' => 'UUID Field'
],
[
'id' => 'name',
'type' => 'text',
'name' => 'Name'
]
]
]);
$response = $this->postJson(route('forms.answer', $form->slug), ['name' => 'Testing', 'uuid_field' => null])
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form submission saved.',
]);
$submissionId = $response->json('submission_id');
expect($submissionId)->toBeString();
$response = $this->getJson(route('forms.fetchSubmission', [$form->slug, $submissionId]))
->assertSuccessful();
$uuid = $response->json('data.uuid_field');
expect(Str::isUuid($uuid))->toBeTrue();
if ($submissionId) {
$formData = $this->generateFormSubmissionData($form, ['submission_id' => $submissionId, 'name' => 'Testing Updated', 'uuid_field' => $uuid]);
$response = $this->postJson(route('forms.answer', $form->slug), $formData)
->assertSuccessful()
->assertJson([
'type' => 'success',
'message' => 'Form submission saved.',
]);
$submissionId2 = $response->json('submission_id');
expect($submissionId2)->toBeString();
expect($submissionId2)->toBe($submissionId);
$response = $this->getJson(route('forms.fetchSubmission', [$form->slug, $submissionId]))
->assertSuccessful();
expect($response->json('data.name'))->toBe('Testing Updated');
$uuid2 = $response->json('data.uuid_field');
expect($uuid2)->toBe($uuid);
}
});

View File

@ -8,14 +8,12 @@
v-if="isScanning"
class="relative w-full"
>
<ClientOnly>
<CameraUpload
:is-barcode-mode="true"
:decoders="decoders"
@stop-webcam="stopScanning"
@barcode-detected="handleBarcodeDetected"
/>
</ClientOnly>
</div>
<div

View File

@ -10,14 +10,12 @@
theme.fileInput.minHeight
]"
>
<ClientOnly>
<CameraUpload
<camera-upload
v-if="cameraUpload"
:theme="theme"
@upload-image="cameraFileUpload"
@stop-webcam="isInWebcam=false"
/>
</ClientOnly>
</div>
<div
v-else

View File

@ -1,10 +1,10 @@
<template>
<InputWrapper v-bind="inputWrapperProps">
<input-wrapper v-bind="inputWrapperProps">
<template #label>
<slot name="label" />
</template>
<span class="inline-block w-full rounded-md shadow-xs">
<span class="inline-block w-full rounded-md shadow-sm">
<button
type="button"
aria-haspopup="listbox"
@ -24,37 +24,52 @@
>
<div
v-if="currentUrl == null"
class="text-gray-600 dark:text-gray-400 flex justify-center"
class="text-gray-600 dark:text-gray-400"
>
<Icon
name="heroicons:cloud-arrow-up"
class="h-5 w-5"
/>
<span class="ml-2">
Upload
</span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5 inline"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
Upload image
</div>
<div
v-else
class=" text-gray-600 dark:text-gray-400 flex"
class="h-6 text-gray-600 dark:text-gray-400 flex"
>
<div class="flex-grow">
<img
:src="tmpFile ?? currentUrl"
class="h-5 rounded shadow-md border"
:src="currentUrl"
class="h-6 rounded shadow-md"
>
</div>
<a
href="#"
class="text-gray-500 hover:text-red-500 flex items-center"
class="hover:text-nt-blue flex"
@click.prevent="clearUrl"
>
<Icon
name="heroicons:trash"
class="h-5 w-5"
/>
</a>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/></svg></a>
</div>
</button>
</span>
@ -90,7 +105,7 @@
v-if="loading"
class="text-gray-600 dark:text-gray-400"
>
<loader class="h-5 w-5 mx-auto m-10" />
<Loader class="h-6 w-6 mx-auto m-10" />
<p class="text-center mt-6">
Uploading your file...
</p>
@ -112,10 +127,20 @@
accept="image/png, image/gif, image/jpeg, image/bmp, image/svg+xml"
@change="manualFileUpload"
>
<Icon
name="heroicons:cloud-arrow-up"
class="x-auto h-24 w-24 text-gray-200"
/>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mx-auto h-24 w-24 text-gray-200"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>
<p class="mt-5 text-sm text-gray-600">
<button
type="button"
@ -136,7 +161,7 @@
</div>
</div>
</modal>
</InputWrapper>
</input-wrapper>
</template>
<script>

View File

@ -1,199 +0,0 @@
<template>
<input-wrapper v-bind="inputWrapperProps">
<template #label>
<slot name="label" />
</template>
<div
class="grid"
:class="[gridClass, { 'gap-2': !seamless }]"
:style="optionStyle"
role="listbox"
:aria-multiselectable="multiple ? 'true' : 'false'"
:tabindex="disabled ? -1 : 0"
@keydown="onKeydown"
ref="root"
>
<button
v-for="(option, idx) in options"
:key="option[optionKey]"
class="flex flex-col items-center justify-center p-1.5 border transition-colors text-gray-500 focus:outline-none"
:class="[
option.class ? (typeof option.class === 'function' ? option.class(isSelected(option)) : option.class) : {},
{
'border-form-color text-form-color bg-form-color/10': isSelected(option),
'hover:bg-gray-100 border-gray-300': !isSelected(option),
'opacity-50 pointer-events-none': disabled || option.disabled,
// Seamless mode: only first and last have radius
'rounded-lg': !seamless,
'rounded-l-lg': seamless && idx === 0,
'rounded-r-lg': seamless && idx === options.length - 1,
// Seamless mode: overlap borders with negative margin, keep all borders
'-ml-px': seamless && idx > 0,
// Seamless mode: z-index hierarchy - selected > hovered/focused > default
'relative z-20': seamless && isSelected(option),
'relative z-10': seamless && !isSelected(option) && focusedIdx === idx,
'relative z-0': seamless && !isSelected(option) && focusedIdx !== idx,
// Add hover z-index for seamless mode (but lower than selected)
'hover:z-10': seamless && !isSelected(option)
}
]"
:aria-selected="isSelected(option) ? 'true' : 'false'"
:tabindex="disabled || option.disabled ? -1 : 0"
:disabled="disabled || option.disabled"
@click="selectOption(option)"
@focus="focusedIdx = idx"
@mouseenter="focusedIdx = idx"
:title="option.tooltip || ''"
role="option"
>
<slot name="icon" :option="option" :selected="isSelected(option)">
<Icon
v-if="option.icon"
:name="isSelected(option) && option.selectedIcon ? option.selectedIcon : option.icon"
:class="[
'w-4 h-4',
option.label ? 'mb-1' : '',
isSelected(option) ? 'text-form-color' : 'text-inherit',
option.iconClass ? (typeof option.iconClass === 'function' ? option.iconClass(isSelected(option)) : option.iconClass) : {}
]"
/>
</slot>
<span
v-if="option.label || !option.icon"
class="text-xs"
:class="{
'text-form-color': isSelected(option),
'text-inherit': !isSelected(option),
}"
>{{ isSelected(option) ? option.selectedLabel ?? option.label : option.label }}</span>
</button>
</div>
<template #help>
<slot name="help" />
</template>
<template #error>
<slot name="error" />
</template>
</input-wrapper>
</template>
<script setup>
import { ref, computed, watch, nextTick } from 'vue'
import { inputProps, useFormInput } from './useFormInput.js'
import InputWrapper from './components/InputWrapper.vue'
/**
* OptionSelectorInput.vue
*
* A form input component for selecting options in a grid layout with icons.
* Integrates with the form system using InputWrapper and useFormInput.
*
* Props:
* - options: Array<{ name, label, icon, selectedIcon?, iconClass?, tooltip?, disabled? }>
* - multiple: Boolean (default: false)
* - optionKey: String (default: 'name')
* - columns: Number (default: 3, for grid layout)
* - seamless: Boolean (default: false, removes gaps and only applies radius to first/last items)
*
* Features:
* - Keyboard navigation (arrow keys, enter/space to select)
* - Focus management
* - Optional tooltips per option
* - Form validation integration
* - Notion-style look by default
* - Seamless mode for connected button appearance
*/
const props = defineProps({
...inputProps,
options: { type: Array, required: true },
multiple: { type: Boolean, default: false },
optionKey: { type: String, default: 'name' },
columns: { type: Number, default: 3 },
seamless: { type: Boolean, default: false }
})
const emit = defineEmits(['update:modelValue', 'focus', 'blur'])
// Use form input composable
const {
compVal,
inputWrapperProps
} = useFormInput(props, { emit })
// Local state
const focusedIdx = ref(-1)
const root = ref(null)
// Computed properties
const gridClass = computed(() => `grid-cols-${props.columns}`)
const optionStyle = computed(() => ({
'--bg-form-color': props.color
}))
// Methods
function isSelected(option) {
if (props.multiple) {
return Array.isArray(compVal.value) && compVal.value.includes(option[props.optionKey])
}
return compVal.value === option[props.optionKey]
}
function selectOption(option) {
if (props.disabled || option.disabled) return
if (props.multiple) {
let newValue = Array.isArray(compVal.value) ? [...compVal.value] : []
const idx = newValue.indexOf(option[props.optionKey])
if (idx > -1) {
newValue.splice(idx, 1)
} else {
newValue.push(option[props.optionKey])
}
compVal.value = newValue
} else {
compVal.value = isSelected(option) ? null : option[props.optionKey]
}
}
function onKeydown(e) {
if (props.disabled) return
const len = props.options.length
if (len === 0) return
if (["ArrowRight", "ArrowDown"].includes(e.key)) {
e.preventDefault()
focusedIdx.value = (focusedIdx.value + 1) % len
focusButton(focusedIdx.value)
} else if (["ArrowLeft", "ArrowUp"].includes(e.key)) {
e.preventDefault()
focusedIdx.value = (focusedIdx.value - 1 + len) % len
focusButton(focusedIdx.value)
} else if (["Enter", " ", "Spacebar"].includes(e.key)) {
e.preventDefault()
if (focusedIdx.value >= 0 && focusedIdx.value < len) {
selectOption(props.options[focusedIdx.value])
}
}
}
function focusButton(idx) {
nextTick(() => {
const btns = root.value?.querySelectorAll('button')
if (btns && btns[idx]) btns[idx].focus()
})
}
// Watchers
watch(compVal, (val) => {
// Keep focus on selected
if (!props.multiple && val != null) {
const idx = props.options.findIndex(opt => opt[props.optionKey] === val)
if (idx !== -1) focusedIdx.value = idx
}
})
</script>

View File

@ -4,7 +4,7 @@
:class="[theme.fileInput.borderRadius]"
>
<video
ref="webcamRef"
id="webcam"
autoplay
playsinline
muted
@ -12,17 +12,17 @@
{ hidden: !isCapturing },
theme.fileInput.minHeight,
theme.fileInput.borderRadius,
'w-full h-full object-cover bg-gray-500'
'w-full h-full object-cover border border-gray-400/30'
]"
webkit-playsinline
/>
<canvas
ref="canvasRef"
id="canvas"
:class="[
{ hidden: !capturedImage },
theme.fileInput.borderRadius,
theme.fileInput.minHeight,
'w-full h-full object-cover'
'w-full h-full object-cover border border-gray-400/30'
]"
/>
@ -31,16 +31,23 @@
v-if="isCapturing && isBarcodeMode"
class="absolute inset-0 pointer-events-none"
>
<!-- Semi-transparent overlay -->
<div class="absolute inset-0 bg-black/30" />
<!-- Scanning area (transparent window) -->
<div
class="absolute inset-0 flex items-strech justify-center px-8 py-12"
class="absolute inset-0 flex items-center justify-center"
style="padding-bottom: 60px;"
>
<div class="flex-grow w-full relative">
<div class="relative w-4/5 h-3/5">
<!-- Transparent window -->
<div class="absolute inset-0 bg-transparent border-0" />
<!-- Corner indicators -->
<div class="absolute top-0 left-0 w-8 h-8 border-t-2 border-l-2 rounded-tl-md border-white" />
<div class="absolute top-0 right-0 w-8 h-8 border-t-2 border-r-2 rounded-tr-md border-white" />
<div class="absolute bottom-0 left-0 w-8 h-8 border-b-2 border-l-2 rounded-bl-md border-white" />
<div class="absolute bottom-0 right-0 w-8 h-8 border-b-2 border-r-2 rounded-br-md border-white" />
<div class="absolute top-0 left-0 w-8 h-8 border-t-2 border-l-2 border-white" />
<div class="absolute top-0 right-0 w-8 h-8 border-t-2 border-r-2 border-white" />
<div class="absolute bottom-0 left-0 w-8 h-8 border-b-2 border-l-2 border-white" />
<div class="absolute bottom-0 right-0 w-8 h-8 border-b-2 border-r-2 border-white" />
</div>
</div>
</div>
@ -145,7 +152,7 @@
<script>
import Webcam from "webcam-easy"
import CachedDefaultTheme from "~/lib/forms/themes/CachedDefaultTheme.js"
import { BrowserMultiFormatReader, DecodeHintType, BarcodeFormat } from '@zxing/library'
import Quagga from 'quagga'
export default {
name: "CameraUpload",
@ -174,7 +181,7 @@ export default {
isCapturing: false,
capturedImage: null,
cameraPermissionStatus: "loading",
zxingReader: null,
quaggaInitialized: false,
currentFacingMode: 'user',
mediaStream: null
}),
@ -190,10 +197,9 @@ export default {
}
},
mounted() {
// For regular camera mode, we still need the webcam.js setup
if (!this.isBarcodeMode) {
this.webcam = new Webcam(this.$refs.webcamRef, "user", this.$refs.canvasRef)
}
const webcamElement = document.getElementById("webcam")
const canvasElement = document.getElementById("canvas")
this.webcam = new Webcam(webcamElement, "user", canvasElement)
this.openCameraUpload()
},
@ -203,27 +209,29 @@ export default {
methods: {
async cleanupCurrentStream() {
if (this.zxingReader) {
this.zxingReader.reset()
this.zxingReader = null
if (this.quaggaInitialized) {
Quagga.stop()
this.quaggaInitialized = false
}
if (this.mediaStream) {
this.mediaStream.getTracks().forEach(track => track.stop())
this.mediaStream = null
}
if (this.webcam) {
this.webcam.stop()
this.webcam = null
}
// Clean up video element if needed
if (this.$refs.webcamRef && this.$refs.webcamRef.srcObject) {
const tracks = this.$refs.webcamRef.srcObject.getTracks()
const webcamElement = document.getElementById("webcam")
if (webcamElement && webcamElement.srcObject) {
const tracks = webcamElement.srcObject.getTracks()
tracks.forEach(track => track.stop())
this.$refs.webcamRef.srcObject = null
webcamElement.srcObject = null
}
},
async switchCamera() {
if (!this.isMobileDevice) return
try {
// Stop current camera and clean up resources
this.cleanupCurrentStream()
@ -232,13 +240,7 @@ export default {
this.currentFacingMode = this.currentFacingMode === 'user' ? 'environment' : 'user'
// Restart camera
if (this.isBarcodeMode) {
setTimeout(() => {
this.initZxingDirect()
}, 500)
} else {
await this.openCameraUpload()
}
await this.openCameraUpload()
} catch (error) {
console.error('Error switching camera:', error)
this.cameraPermissionStatus = "unknown"
@ -250,18 +252,15 @@ export default {
this.capturedImage = null
try {
if (this.isBarcodeMode) {
// For barcode mode, let ZXing handle everything
this.cameraPermissionStatus = "allowed"
setTimeout(() => {
this.initZxingDirect()
}, 500)
return
}
const webcamElement = document.getElementById("webcam")
const canvasElement = document.getElementById("canvas")
// Regular camera mode - use existing logic
// Determine the facing mode to use
let facingMode = this.currentFacingMode
if (this.isBarcodeMode && this.currentFacingMode === 'user') {
// Force environment mode for barcode scanning
facingMode = 'environment'
}
// Create constraints based on device capabilities
const constraints = {
@ -294,26 +293,25 @@ export default {
}
this.mediaStream = stream // Store the stream reference
this.$refs.webcamRef.srcObject = stream
webcamElement.srcObject = stream
this.webcam = new Webcam(
this.$refs.webcamRef,
webcamElement,
facingMode,
this.$refs.canvasRef
canvasElement
)
await new Promise((resolve) => {
this.$refs.webcamRef.onloadedmetadata = () => {
this.$refs.webcamRef.play().then(() => {
resolve()
}).catch(err => {
console.error('Error playing video:', err)
resolve() // Continue anyway
})
webcamElement.onloadedmetadata = () => {
webcamElement.play()
resolve()
}
})
this.cameraPermissionStatus = "allowed"
if (this.isBarcodeMode) {
this.initQuagga()
}
} catch (err) {
console.error('Camera error:', err)
if (err.name === 'NotAllowedError' || err.toString().includes('Permission denied')) {
@ -323,80 +321,47 @@ export default {
}
}
},
initZxingDirect() {
if (this.zxingReader) {
this.zxingReader.reset()
this.zxingReader = null
initQuagga() {
if (!this.quaggaInitialized) {
Quagga.init({
inputStream: {
name: "Live",
type: "LiveStream",
target: document.getElementById("webcam"),
constraints: {
facingMode: this.currentFacingMode,
width: { min: 640 },
height: { min: 480 },
aspectRatio: { min: 1, max: 2 }
},
},
locator: {
patchSize: "medium",
halfSample: true
},
numOfWorkers: navigator.hardwareConcurrency || 4,
frequency: 10,
decoder: {
readers: this.decoders || []
},
locate: true
}, (err) => {
if (err) {
console.error('Quagga initialization failed:', err)
return
}
this.quaggaInitialized = true
Quagga.start()
Quagga.onDetected((result) => {
if (result.codeResult) {
this.$emit('barcodeDetected', result.codeResult.code)
this.cancelCamera()
}
})
})
}
const hints = new Map()
const formats = (this.decoders || []).map(decoder => {
// Map decoder strings to BarcodeFormat enum values
// Remove _reader suffix for mapping
const cleanDecoder = decoder.replace('_reader', '').toLowerCase()
switch(cleanDecoder) {
case 'ean_8': return BarcodeFormat.EAN_8
case 'ean':
case 'ean_13': return BarcodeFormat.EAN_13
case 'upc':
case 'upc_a': return BarcodeFormat.UPC_A
case 'upc_e': return BarcodeFormat.UPC_E
case 'code_39': return BarcodeFormat.CODE_39
case 'code_93': return BarcodeFormat.CODE_93
case 'code_128': return BarcodeFormat.CODE_128
case 'codabar': return BarcodeFormat.CODABAR
case 'itf': return BarcodeFormat.ITF
case 'qr':
case 'qr_code': return BarcodeFormat.QR_CODE
case 'data_matrix': return BarcodeFormat.DATA_MATRIX
case 'aztec': return BarcodeFormat.AZTEC
case 'pdf_417': return BarcodeFormat.PDF_417
default:
console.warn('Unsupported barcode format:', decoder)
return null
}
}).filter(format => format !== null)
if (formats.length > 0) {
hints.set(DecodeHintType.POSSIBLE_FORMATS, formats)
}
this.zxingReader = new BrowserMultiFormatReader(hints)
// Use simple constraints approach instead of device enumeration
const facingMode = this.isMobileDevice && this.currentFacingMode === 'user' ? 'environment' : this.currentFacingMode
const constraints = {
audio: false,
video: {
facingMode: facingMode,
width: { ideal: 1280 },
height: { ideal: 720 }
}
}
// Use ZXing's decodeFromConstraints method
this.zxingReader.decodeFromConstraints(constraints, this.$refs.webcamRef, (result, error) => {
if (result) {
this.$emit('barcodeDetected', result.text)
}
// Don't log NotFoundException errors - they're expected during scanning
// Only log other types of errors
else if (error && error.name && !error.name.includes('NotFoundException') && !error.message?.includes('No MultiFormat Readers')) {
console.error('ZXing decoding error:', error.name, error.message)
}
})
.then(() => {
this.cameraPermissionStatus = "allowed"
})
.catch(err => {
console.error('Camera error in ZXing Direct:', err)
if (err.name === 'NotAllowedError' || err.toString().includes('Permission denied')) {
this.cameraPermissionStatus = "blocked"
} else {
this.cameraPermissionStatus = "unknown"
}
})
},
cancelCamera() {
this.isCapturing = false
@ -405,10 +370,6 @@ export default {
this.$emit("stopWebcam")
},
processCapturedImage() {
if (!this.webcam) {
return
}
this.capturedImage = this.webcam.snap()
this.isCapturing = false
this.webcam.stop()

View File

@ -246,8 +246,6 @@ const isAutoSubmit = ref(import.meta.client && window.location.href.includes('au
// Create a reactive reference directly from the prop
const darkModeRef = toRef(props, 'darkMode')
// Create a reactive reference for the mode prop
const modeRef = toRef(props, 'mode')
// Add back the local theme computation
const theme = computed(() => {
@ -260,8 +258,7 @@ const theme = computed(() => {
let formManager = null
if (props.form) {
formManager = useFormManager(props.form, props.mode, {
darkMode: darkModeRef,
mode: modeRef
darkMode: darkModeRef
})
formManager.initialize({
submissionId: submissionId.value,

View File

@ -17,172 +17,145 @@
:form="form"
label="Form Theme"
/>
<color-input
name="color"
:form="form"
label="Accent Color"
class="my-4"
>
<template #label>
<InputLabel>Accent Color - <a
href="#" class="text-blue-500"
@click.prevent="form.color = DEFAULT_COLOR"
>Reset</a></InputLabel>
<template #help>
<InputHelp>
<span class="text-gray-500">
Color (for buttons & inputs border) - <a
class="text-blue-500"
href="#"
@click.prevent="form.color = DEFAULT_COLOR"
>Reset</a>
</span>
</InputHelp>
</template>
</color-input>
<OptionSelectorInput
v-model="form.dark_mode"
:form="form"
<select-input
name="dark_mode"
label="Color Mode"
:options="[
{ name: 'auto', label: 'System', icon: 'i-heroicons-computer-desktop' },
{ name: 'light', label: 'Light', icon: 'i-heroicons-sun' },
{ name: 'dark', label: 'Dark', icon: 'i-heroicons-moon' },
{ name: 'Auto', value: 'auto' },
{ name: 'Light Mode', value: 'light' },
{ name: 'Dark Mode', value: 'dark' },
]"
:multiple="false"
:columns="3"
class="mb-4"
:form="form"
label="Color Mode"
help="Use Auto to use device system preferences"
/>
<EditorSectionHeader
icon="octicon:typography-16"
title="Text & Language"
title="Typography"
/>
<div class="grid grid-cols-2 gap-4">
<div class="flex-grow my-1" v-if="useFeatureFlag('services.google.fonts')">
<label class="text-gray-700 font-semibold text-sm mb-1 block">Font Family</label>
<v-button
color="white"
class="w-full py-1.5"
size="small"
@click="showGoogleFontPicker = true"
>
<span :style="{ 'font-family': (form.font_family ? form.font_family + ' !important' : null) }">
{{ form.font_family || 'Default' }}
</span>
</v-button>
<GoogleFontPicker
:show="showGoogleFontPicker"
:font="form.font_family || null"
@close="showGoogleFontPicker = false"
@apply="onApplyFont"
/>
</div>
<div class="flex-grow">
<select-input
name="language"
searchable
:options="availableLocales"
:form="form"
label="Language"
/>
</div>
</div>
<ToggleSwitchInput
name="layout_rtl"
:form="form"
class="mt-4"
label="Right-to-Left Layout"
/>
<template v-if="useFeatureFlag('services.google.fonts')">
<label class="text-gray-700 font-medium text-sm">Font Style</label>
<v-button
color="white"
class="w-full mb-4"
size="small"
@click="showGoogleFontPicker = true"
>
<span :style="{ 'font-family': (form.font_family?form.font_family+' !important':null) }">
{{ form.font_family || 'Default' }}
</span>
</v-button>
<GoogleFontPicker
:show="showGoogleFontPicker"
:font="form.font_family || null"
@close="showGoogleFontPicker=false"
@apply="onApplyFont"
/>
</template>
<toggle-switch-input
name="uppercase_labels"
:form="form"
class="mt-4"
label="Uppercase Input Labels"
/>
<select-input
name="language"
class="mt-4"
searchable
:options="availableLocales"
:form="form"
label="Form Language"
/>
<EditorSectionHeader
icon="heroicons:rectangle-stack-16-solid"
title="Layout & Sizing"
/>
<div class="flex space-x-4 justify-stretch">
<div class="flex-grow">
<OptionSelectorInput
seamless
label="Input Size"
v-model="form.size"
:form="form"
name="size"
:options="[
{ name: 'sm', label:'S'},
{ name: 'md', label:'M' },
{ name: 'lg', label:'L' },
]"
:multiple="false"
:columns="3"
class="mb-4"
/>
</div>
<select-input
name="size"
class="flex-grow"
:options="[
{ name: 'Small', value: 'sm' },
{ name: 'Medium', value: 'md' },
{ name: 'Large', value: 'lg' },
]"
:form="form"
label="Input Size"
/>
<div class="flex-grow">
<OptionSelectorInput
label="Input Roundness"
v-model="form.border_radius"
seamless
:form="form"
name="border_radius"
:options="[
{ name: 'none', icon: 'i-tabler-border-corner-square' },
{ name: 'small', icon: 'i-tabler-border-corner-rounded' },
{ name: 'full', icon: 'i-tabler-border-corner-pill' },
]"
:multiple="false"
:columns="3"
class="mb-4"
/>
</div>
<select-input
name="border_radius"
class="flex-grow"
:options="[
{ name: 'None', value: 'none' },
{ name: 'Small', value: 'small' },
{ name: 'Full', value: 'full' },
]"
:form="form"
label="Input Roundness"
/>
</div>
<OptionSelectorInput
v-model="form.width"
label="Form Width"
:form="form"
<select-input
name="width"
seamless
:options="[
{ name: 'centered', label: 'Centered' },
{ name: 'full', label: 'Full Width' },
{ name: 'Centered', value: 'centered' },
{ name: 'Full Width', value: 'full' },
]"
:multiple="false"
:columns="2"
class="mb-4 w-2/3"
:form="form"
label="Form Width"
help="Useful when embedding your form"
/>
<ToggleSwitchInput
name="layout_rtl"
:form="form"
class="mt-4"
label="Right-to-Left Layout"
help="Adjusts layout for RTL languages"
/>
<EditorSectionHeader
icon="heroicons:tag-16-solid"
title="Branding & Media"
/>
<div class="grid grid-cols-2 gap-4">
<image-input
name="logo_picture"
:form="form"
label="Logo"
:required="false"
/>
<image-input
name="cover_picture"
:form="form"
label="Cover (~1500px)"
:required="false"
/>
</div>
<image-input
name="logo_picture"
:form="form"
label="Logo"
help="Not visible when form is embedded"
:required="false"
/>
<image-input
name="cover_picture"
:form="form"
label="Cover image"
help="Not visible when form is embedded"
/>
<toggle-switch-input
name="no_branding"
:form="form"
class="mt-4"
@update:model-value="onChangeNoBranding"
>
<template #label>
<span class="text-sm">
Hide OpnForm Branding
Remove OpnForm Branding
</span>
<pro-tag
upgrade-modal-title="Upgrade today to remove OpnForm branding"
@ -209,7 +182,7 @@
name="transparent_background"
:form="form"
label="Transparent Background"
help="When form is embedded"
help="Only applies when form is embedded"
/>
<toggle-switch-input
name="confetti_on_submission"

View File

@ -18,7 +18,7 @@
class="border rounded-lg bg-white dark:bg-notion-dark w-full block shadow-sm transition-all flex flex-col"
:class="{ 'max-w-5xl': !isExpanded, 'h-full': isExpanded }"
>
<div class="w-full bg-white dark:bg-gray-950 border-b border-gray-300 dark:border-blue-900 dark:border-gray-700 rounded-t-lg p-1.5 pl-4 pr-1.5 flex items-center gap-x-1.5">
<div class="w-full bg-white dark:bg-gray-950 border-b border-gray-300 dark:border-blue-900 dark:border-gray-700 rounded-t-lg p-1.5 px-4 flex items-center gap-x-1.5">
<div class="bg-red-500 rounded-full w-2.5 h-2.5" />
<div class="bg-yellow-500 rounded-full w-2.5 h-2.5" />
<div class="bg-green-500 rounded-full w-2.5 h-2.5" />

View File

@ -641,13 +641,12 @@ export default {
],
allCountries: countryCodes,
barcodeDecodersOptions: [
{ name: 'QR Code', value: 'qr_reader' },
{ name: 'EAN-13 (European Article Number)', value: 'ean_reader' },
{ name: 'EAN-8 (European Article Number)', value: 'ean_8_reader' },
{ name: 'UPC-A (Universal Product Code)', value: 'upc_reader' },
{ name: 'UPC-E (Universal Product Code)', value: 'upc_e_reader' },
{ name: 'Code 128', value: 'code_128_reader' },
{ name: 'Code 39', value: 'code_39_reader' }
{ name: 'Code 39', value: 'code_39_reader' },
]
}
},

View File

@ -1,61 +1,87 @@
<template>
<OptionSelectorInput
:options="availableOptions"
v-model="selectedOption"
:multiple="false"
:disabled="false"
:columns="3"
name="field_state"
/>
<div class="grid grid-cols-3 gap-2">
<button
v-for="option in availableOptions"
:key="option.name"
class="flex flex-col items-center justify-center p-1.5 border rounded-lg transition-colors text-gray-500"
:class="[
option.class ? (typeof option.class === 'function' ? option.class(isSelected(option.name)) : option.class) : {},
{
'border-blue-500 bg-blue-50': isSelected(option.name),
'hover:bg-gray-100 border-gray-300': !isSelected(option.name)
}
]"
@click="toggleOption(option.name)"
>
<Icon
:name="isSelected(option.name) && option.selectedIcon ? option.selectedIcon : option.icon"
:class="[
'w-4 h-4 mb-1',
{
'text-blue-500': isSelected(option.name),
'text-inherit': !isSelected(option.name),
},
option.iconClass ? (typeof option.iconClass === 'function' ? option.iconClass(isSelected(option.name)) : option.iconClass) : {}
]"
/>
<span
class="text-xs"
:class="{
'text-blue-500': isSelected(option.name),
'text-inherit': !isSelected(option.name),
}"
>{{ isSelected(option.name) ? option.selectedLabel ?? option.label : option.label }}</span>
</button>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
field: {
type: Object,
required: true
},
canBeDisabled: {
type: Boolean,
default: true
},
canBeRequired: {
type: Boolean,
default: true
},
canBeHidden: {
type: Boolean,
default: true
}
field: {
type: Object,
required: true
},
canBeDisabled: {
type: Boolean,
default: true
},
canBeRequired: {
type: Boolean,
default: true
},
canBeHidden: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['update:field'])
const options = [
defineEmits(['update:field'])
const options = ref([
{
name: 'required',
label: 'Required',
icon: 'ph:asterisk-bold',
selectedIcon: 'ph:asterisk-bold',
icon: 'i-ph-asterisk-bold',
selectedIcon: 'i-ph-asterisk-bold',
iconClass: (isActive) => isActive ? 'text-red-500' : '',
},
{
name: 'hidden',
label: 'Hidden',
icon: 'heroicons:eye',
selectedIcon: 'heroicons:eye-slash-solid',
icon: 'i-heroicons-eye',
selectedIcon: 'i-heroicons-eye-slash-solid',
},
{
name: 'disabled',
label: 'Disabled',
icon: 'heroicons:lock-open',
selectedIcon: 'heroicons:lock-closed-solid',
icon: 'i-heroicons-lock-open',
selectedIcon: 'i-heroicons-lock-closed-solid',
}
]
])
const availableOptions = computed(() => {
return options.filter(option => {
return options.value.filter(option => {
if (option.name === 'disabled') return props.canBeDisabled
if (option.name === 'required') return props.canBeRequired
if (option.name === 'hidden') return props.canBeHidden
@ -63,34 +89,28 @@ const availableOptions = computed(() => {
})
})
const selectedOption = computed({
get() {
// Only one can be true at a time, priority: required > hidden > disabled
if (props.field.required) return 'required'
if (props.field.hidden) return 'hidden'
if (props.field.disabled) return 'disabled'
return null
},
set(optionName) {
// Reset all
props.field.required = false
const isSelected = (optionName) => {
return props.field[optionName]
}
const toggleOption = (optionName) => {
const newValue = !props.field[optionName]
if (optionName === 'required' && newValue) {
props.field.hidden = false
} else if (optionName === 'hidden' && newValue) {
props.field.required = false
props.field.disabled = false
// Apply business logic
if (optionName === 'required') {
props.field.required = true
props.field.hidden = false
} else if (optionName === 'hidden') {
props.field.hidden = true
props.field.required = false
props.field.disabled = false
props.field.generates_uuid = false
props.field.generates_auto_increment_id = false
} else if (optionName === 'disabled') {
props.field.disabled = true
props.field.hidden = false
}
emit('update:field', { ...props.field })
props.field.generates_uuid = false
props.field.generates_auto_increment_id = false
} else if (optionName === 'disabled' && newValue) {
props.field.hidden = false
}
})
if ((optionName === 'disabled' && props.canBeDisabled) ||
(optionName === 'required' && props.canBeRequired) ||
(optionName === 'hidden' && props.canBeHidden)) {
props.field[optionName] = newValue
}
}
</script>

View File

@ -4,9 +4,6 @@
<div class="flex mt-2 items-center">
<p class="text-sm text-gray-600 dark:text-gray-400 text-center w-full">
© Copyright {{ currYear }}. All Rights Reserved
<span v-if="version">
<br>Version {{ version }}
</span>
</p>
</div>
<div class="flex justify-center mt-5 md:mt-0">
@ -85,14 +82,22 @@
</div>
</template>
<script setup>
<script>
import { computed } from "vue"
import opnformConfig from "~/opnform.config.js"
const authStore = useAuthStore()
export default {
setup() {
const authStore = useAuthStore()
return {
user: computed(() => authStore.user),
appStore: useAppStore(),
opnformConfig,
}
},
const user = computed(() => authStore.user)
const currYear = ref(new Date().getFullYear())
// Use the reactive version for proper template reactivity
const version = computed(() => useFeatureFlag('version'))
data: () => ({
currYear: new Date().getFullYear(),
}),
}
</script>

View File

@ -207,22 +207,9 @@
"text_class": "text-pink-900",
"is_input": true,
"default_values": {
"decoders": ["qr_reader", "ean_reader", "ean_8_reader"]
"decoders": ["ean_reader", "ean_8_reader"]
}
},
"qrcode": {
"name": "qrcode",
"title": "QR Code Reader",
"icon": "i-material-symbols-qr-code-scanner-rounded",
"default_block_name": "Scan QR Code",
"bg_class": "bg-pink-100",
"text_class": "text-pink-900",
"is_input": true,
"default_values": {
"decoders": ["qr_reader", "ean_reader", "ean_8_reader"]
},
"actual_input": "barcode"
},
"payment": {
"name": "payment",
"title": "Payment",

View File

@ -19,17 +19,11 @@ import { cloneDeep } from 'lodash'
* Initializes and coordinates various form composables (Structure, Init, Validation, etc.)
* based on the provided form configuration and mode.
*/
export function useFormManager(initialFormConfig, initialMode = FormMode.LIVE, options = {}) {
export function useFormManager(initialFormConfig, mode = FormMode.LIVE, options = {}) {
// --- Reactive State ---
const config = ref(initialFormConfig) // Use ref for potentially replaceable config
const form = useForm() // Core vForm instance
// Make mode reactive - accept either a ref or a static value
const mode = options.mode && typeof options.mode === 'object' && 'value' in options.mode
? options.mode
: ref(initialMode)
const strategy = computed(() => createFormModeStrategy(mode.value)) // Strategy based on reactive mode
const strategy = computed(() => createFormModeStrategy(mode)) // Strategy based on mode
// Use the passed darkMode ref if it's a ref, otherwise create a new ref
const darkMode = options.darkMode && typeof options.darkMode === 'object' && 'value' in options.darkMode
@ -123,7 +117,7 @@ export function useFormManager(initialFormConfig, initialMode = FormMode.LIVE, o
const paymentBlock = structure.currentPagePaymentBlock.value
if (paymentBlock) {
// In editor/test mode (not LIVE), skip payment validation
const isPaymentRequired = mode.value === FormMode.LIVE ? !!paymentBlock.required : false
const isPaymentRequired = mode === FormMode.LIVE ? !!paymentBlock.required : false
// Pass required refs if Stripe needs them now (unlikely for just intent creation)
const paymentResult = await payment.processPayment(paymentBlock, isPaymentRequired)
@ -185,7 +179,7 @@ export function useFormManager(initialFormConfig, initialMode = FormMode.LIVE, o
if (paymentBlock) {
// In editor/test mode (not LIVE), skip payment validation
const isPaymentRequired = mode.value === FormMode.LIVE ? !!paymentBlock.required : false
const isPaymentRequired = mode === FormMode.LIVE ? !!paymentBlock.required : false
const paymentResult = await payment.processPayment(paymentBlock, isPaymentRequired)
@ -322,8 +316,6 @@ export function useFormManager(initialFormConfig, initialMode = FormMode.LIVE, o
// UI-related properties
darkMode, // Dark mode setting
setDarkMode: (isDark) => { darkMode.value = isDark }, // Method to update dark mode
mode, // Form mode setting (ref)
setMode: (newMode) => { mode.value = newMode }, // Method to update form mode
// Composables (Expose if direct access needed, often not necessary)
structure,

View File

@ -1,5 +1,11 @@
export default defineNuxtRouteMiddleware(async () => {
const authStore = useAuthStore()
const featureFlagsStore = useFeatureFlagsStore()
// Ensure feature flags are loaded
if (!featureFlagsStore.isLoaded) {
await featureFlagsStore.fetchFlags()
}
if (useFeatureFlag('self_hosted')) {
if (authStore.check && authStore.user?.email === 'admin@opnform.com') {

View File

@ -22,7 +22,7 @@ export default defineNuxtConfig({
],
build: {
transpile: ["vue-notion", "query-builder-vue-3", "vue-signature-pad", "@zxing/library"],
transpile: ["vue-notion", "query-builder-vue-3", "vue-signature-pad"],
},
i18n: {

643
client/package-lock.json generated
View File

@ -21,7 +21,6 @@
"@vueuse/integrations": "^11.2.0",
"@vueuse/motion": "^2.2.6",
"@vueuse/nuxt": "^11.2.0",
"@zxing/library": "^0.21.3",
"amplitude-js": "^8.21.9",
"chart.js": "^4.4.5",
"clone-deep": "^4.0.1",
@ -37,6 +36,7 @@
"pinia": "^3.0.2",
"prismjs": "^1.29.0",
"qrcode": "^1.5.4",
"quagga": "^0.12.1",
"query-builder-vue-3": "^1.0.1",
"quill": "^2.0.2",
"tailwind-merge": "^2.5.4",
@ -57,7 +57,6 @@
"@iconify-json/clarity": "^1.2.1",
"@iconify-json/ic": "^1.2.1",
"@iconify-json/octicon": "^1.2.1",
"@iconify-json/tabler": "^1.2.1",
"@nuxt/eslint-config": "^1.3.0",
"@nuxt/icon": "^1.12.0",
"@nuxtjs/i18n": "^9.0.0",
@ -1589,15 +1588,6 @@
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/tabler": {
"version": "1.2.18",
"resolved": "https://registry.npmjs.org/@iconify-json/tabler/-/tabler-1.2.18.tgz",
"integrity": "sha512-W+8qiJhJpb4dmBw3P7JSM9QhGsFG8GIS3BJWAmrJ/92rzK6NPGUOPfGmswoO+/MuPzQV96ColY9lcUktUKv0pg==",
"dev": true,
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify/collections": {
"version": "1.0.546",
"resolved": "https://registry.npmjs.org/@iconify/collections/-/collections-1.0.546.tgz",
@ -9142,26 +9132,6 @@
"node": ">= 10"
}
},
"node_modules/@zxing/library": {
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.21.3.tgz",
"integrity": "sha512-hZHqFe2JyH/ZxviJZosZjV+2s6EDSY0O24R+FQmlWZBZXP9IqMo7S3nb3+2LBWxodJQkSurdQGnqE7KXqrYgow==",
"dependencies": {
"ts-custom-error": "^3.2.1"
},
"engines": {
"node": ">= 10.4.0"
},
"optionalDependencies": {
"@zxing/text-encoding": "~0.9.0"
}
},
"node_modules/@zxing/text-encoding": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
"integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
"optional": true
},
"node_modules/abbrev": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz",
@ -9244,7 +9214,6 @@
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
@ -9459,6 +9428,24 @@
"node": ">=8"
}
},
"node_modules/asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"license": "MIT",
"dependencies": {
"safer-buffer": "~2.1.0"
}
},
"node_modules/assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
"license": "MIT",
"engines": {
"node": ">=0.8"
}
},
"node_modules/ast-kit": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-1.4.3.tgz",
@ -9509,6 +9496,12 @@
"integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==",
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/at-least-node": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
@ -9555,6 +9548,21 @@
"postcss": "^8.1.0"
}
},
"node_modules/aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
"integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==",
"license": "Apache-2.0",
"engines": {
"node": "*"
}
},
"node_modules/aws4": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz",
"integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==",
"license": "MIT"
},
"node_modules/b4a": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
@ -9594,6 +9602,15 @@
],
"license": "MIT"
},
"node_modules/bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
"license": "BSD-3-Clause",
"dependencies": {
"tweetnacl": "^0.14.3"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -10004,6 +10021,12 @@
],
"license": "CC-BY-4.0"
},
"node_modules/caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==",
"license": "Apache-2.0"
},
"node_modules/chalk": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
@ -10264,6 +10287,18 @@
"text-hex": "1.0.x"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz",
@ -10745,6 +10780,33 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/cwise-compiler": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/cwise-compiler/-/cwise-compiler-1.1.3.tgz",
"integrity": "sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==",
"license": "MIT",
"dependencies": {
"uniq": "^1.0.0"
}
},
"node_modules/dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
"license": "MIT",
"dependencies": {
"assert-plus": "^1.0.0"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/data-uri-to-buffer": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-0.0.3.tgz",
"integrity": "sha512-Cp+jOa8QJef5nXS5hU7M1DWzXPEIoVR3kbV0dQuVGwROZg8bGf1DcCnkmajBTnvghTtSNMUdRrPjgaT6ZQucbw==",
"license": "MIT"
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
@ -10919,6 +10981,15 @@
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@ -11335,6 +11406,16 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"license": "MIT"
},
"node_modules/ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
"integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
"license": "MIT",
"dependencies": {
"jsbn": "~0.1.0",
"safer-buffer": "^2.1.0"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -12232,6 +12313,12 @@
"integrity": "sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==",
"license": "MIT"
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
"node_modules/externality": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/externality/-/externality-1.0.2.tgz",
@ -12285,11 +12372,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/extsprintf": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
"engines": [
"node >=0.6.0"
],
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"devOptional": true,
"license": "MIT"
},
"node_modules/fast-diff": {
@ -12336,7 +12431,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"devOptional": true,
"license": "MIT"
},
"node_modules/fast-levenshtein": {
@ -12548,6 +12642,50 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==",
"license": "Apache-2.0",
"engines": {
"node": "*"
}
},
"node_modules/form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 0.12"
}
},
"node_modules/form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@ -12727,6 +12865,55 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-pixels": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/get-pixels/-/get-pixels-3.3.3.tgz",
"integrity": "sha512-5kyGBn90i9tSMUVHTqkgCHsoWoR+/lGbl4yC83Gefyr0HLIhgSWEx/2F/3YgsZ7UpYNuM6pDhDK7zebrUJ5nXg==",
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "0.0.3",
"jpeg-js": "^0.4.1",
"mime-types": "^2.0.1",
"ndarray": "^1.0.13",
"ndarray-pack": "^1.1.1",
"node-bitmap": "0.0.1",
"omggif": "^1.0.5",
"parse-data-uri": "^0.2.0",
"pngjs": "^3.3.3",
"request": "^2.44.0",
"through": "^2.3.4"
}
},
"node_modules/get-pixels/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/get-pixels/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/get-pixels/node_modules/pngjs": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
"integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/get-port-please": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz",
@ -12771,6 +12958,15 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/getpass": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
"integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
"license": "MIT",
"dependencies": {
"assert-plus": "^1.0.0"
}
},
"node_modules/giget": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
@ -12807,6 +13003,24 @@
"git-up": "^8.1.0"
}
},
"node_modules/gl-mat2": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gl-mat2/-/gl-mat2-1.0.1.tgz",
"integrity": "sha512-oHgZ3DalAo9qAhMZM9QigXosqotcUCsgxarwrinipaqfSHvacI79Dzs72gY+oT4Td1kDQKEsG0RyX6mb02VVHA==",
"license": "zlib"
},
"node_modules/gl-vec2": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/gl-vec2/-/gl-vec2-1.3.0.tgz",
"integrity": "sha512-YiqaAuNsheWmUV0Sa8k94kBB0D6RWjwZztyO+trEYS8KzJ6OQB/4686gdrf59wld4hHFIvaxynO3nRxpk1Ij/A==",
"license": "zlib"
},
"node_modules/gl-vec3": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/gl-vec3/-/gl-vec3-1.1.3.tgz",
"integrity": "sha512-jduKUqT0SGH02l8Yl+mV1yVsDfYgQAJyXGxkJQGyxPLHRiW25DwVIRPt6uvhrEMHftJfqhqKthRcyZqNEl9Xdw==",
"license": "zlib"
},
"node_modules/glob": {
"version": "9.3.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz",
@ -12985,6 +13199,29 @@
"h3": "^1.6.0"
}
},
"node_modules/har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
"integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==",
"license": "ISC",
"engines": {
"node": ">=4"
}
},
"node_modules/har-validator": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
"integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
"deprecated": "this library is no longer supported",
"license": "MIT",
"dependencies": {
"ajv": "^6.12.3",
"har-schema": "^2.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -13136,6 +13373,21 @@
"node": ">= 0.12.0"
}
},
"node_modules/http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
"integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==",
"license": "MIT",
"dependencies": {
"assert-plus": "^1.0.0",
"jsprim": "^1.2.2",
"sshpk": "^1.7.0"
},
"engines": {
"node": ">=0.8",
"npm": ">=1.3.7"
}
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
@ -13345,6 +13597,12 @@
"url": "https://opencollective.com/ioredis"
}
},
"node_modules/iota-array": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz",
"integrity": "sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==",
"license": "MIT"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@ -13382,6 +13640,12 @@
"node": ">=8"
}
},
"node_modules/is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
"license": "MIT"
},
"node_modules/is-builtin-module": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz",
@ -13613,6 +13877,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
"license": "MIT"
},
"node_modules/is-url": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
@ -13694,6 +13964,12 @@
"node": ">=0.10.0"
}
},
"node_modules/isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==",
"license": "MIT"
},
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
@ -13718,6 +13994,12 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/jpeg-js": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
"license": "BSD-3-Clause"
},
"node_modules/js-sha256": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.10.1.tgz",
@ -13743,6 +14025,12 @@
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==",
"license": "MIT"
},
"node_modules/jsdoc-type-pratt-parser": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz",
@ -13772,11 +14060,16 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"license": "(AFL-2.1 OR BSD-3-Clause)"
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"devOptional": true,
"license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
@ -13786,6 +14079,12 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==",
"license": "ISC"
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@ -13847,6 +14146,21 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/jsprim": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
"integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
"license": "MIT",
"dependencies": {
"assert-plus": "1.0.0",
"extsprintf": "1.3.0",
"json-schema": "0.4.0",
"verror": "1.10.0"
},
"engines": {
"node": ">=0.6.0"
}
},
"node_modules/junk": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/junk/-/junk-4.0.1.tgz",
@ -14932,6 +15246,32 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/ndarray": {
"version": "1.0.19",
"resolved": "https://registry.npmjs.org/ndarray/-/ndarray-1.0.19.tgz",
"integrity": "sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ==",
"license": "MIT",
"dependencies": {
"iota-array": "^1.0.0",
"is-buffer": "^1.0.2"
}
},
"node_modules/ndarray-linear-interpolate": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/ndarray-linear-interpolate/-/ndarray-linear-interpolate-1.0.0.tgz",
"integrity": "sha512-UN0f4+6XWsQzJ2pP5gVp+kKn5tJed6mA3K/L50uO619+7LKrjcSNdcerhpqxYaSkbxNJuEN76N05yBBJySnZDw==",
"license": "MIT"
},
"node_modules/ndarray-pack": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ndarray-pack/-/ndarray-pack-1.2.1.tgz",
"integrity": "sha512-51cECUJMT0rUZNQa09EoKsnFeDL4x2dHRT0VR5U2H5ZgEcm95ZDWcMA5JShroXjHOejmAD/fg8+H+OvUnVXz2g==",
"license": "MIT",
"dependencies": {
"cwise-compiler": "^1.1.2",
"ndarray": "^1.0.13"
}
},
"node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
@ -15543,6 +15883,14 @@
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT"
},
"node_modules/node-bitmap": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/node-bitmap/-/node-bitmap-0.0.1.tgz",
"integrity": "sha512-Jx5lPaaLdIaOsj2mVLWMWulXF6GQVdyLvNSxmiYCvZ8Ma2hfKX0POoR2kgKOqz+oFsRreq0yYZjQ2wjE9VNzCA==",
"engines": {
"node": ">=v0.6.5"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@ -16517,6 +16865,15 @@
"integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
"license": "MIT"
},
"node_modules/oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==",
"license": "Apache-2.0",
"engines": {
"node": "*"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -16570,6 +16927,12 @@
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
"license": "MIT"
},
"node_modules/omggif": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz",
"integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==",
"license": "MIT"
},
"node_modules/on-change": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/on-change/-/on-change-5.0.1.tgz",
@ -16868,6 +17231,15 @@
"node": ">=6"
}
},
"node_modules/parse-data-uri": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/parse-data-uri/-/parse-data-uri-0.2.0.tgz",
"integrity": "sha512-uOtts8NqDcaCt1rIsO3VFDRsAfgE4c6osG4d9z3l4dCBlxYFzni6Di/oNU270SDrjkfZuUvLZx1rxMyqh46Y9w==",
"license": "ISC",
"dependencies": {
"data-uri-to-buffer": "0.0.3"
}
},
"node_modules/parse-gitignore": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/parse-gitignore/-/parse-gitignore-2.0.0.tgz",
@ -17046,6 +17418,12 @@
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"license": "MIT"
},
"node_modules/performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
@ -18153,6 +18531,18 @@
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"funding": {
"url": "https://github.com/sponsors/lupomontero"
}
},
"node_modules/pump": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
@ -18167,7 +18557,6 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6"
@ -18344,6 +18733,24 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/quagga": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/quagga/-/quagga-0.12.1.tgz",
"integrity": "sha512-bb2N6eT7ss6Bg27sxQgv/CT96KQBkXa+4YeS1W8bhsaXxoWp8zOQbrOwFWEPxPDTNaWEl7hTs3ZB7OC4k3EY3Q==",
"license": "MIT",
"dependencies": {
"get-pixels": "^3.2.3",
"gl-mat2": "^1.0.0",
"gl-vec2": "^1.0.0",
"gl-vec3": "^1.0.3",
"lodash": "^4.17.4",
"ndarray": "^1.0.18",
"ndarray-linear-interpolate": "^1.0.0"
},
"engines": {
"node": ">= 4.0"
}
},
"node_modules/quansync": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz",
@ -18781,6 +19188,78 @@
"node": ">=8"
}
},
"node_modules/request": {
"version": "2.88.2",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
"integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
"deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142",
"license": "Apache-2.0",
"dependencies": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
"caseless": "~0.12.0",
"combined-stream": "~1.0.6",
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"har-validator": "~5.1.3",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"oauth-sign": "~0.9.0",
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.5.0",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/request/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/request/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/request/node_modules/qs": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
"integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.6"
}
},
"node_modules/request/node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"license": "MIT",
"bin": {
"uuid": "bin/uuid"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -19110,7 +19589,6 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"devOptional": true,
"license": "MIT"
},
"node_modules/sass": {
@ -19551,6 +20029,31 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/sshpk": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
"integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==",
"license": "MIT",
"dependencies": {
"asn1": "~0.2.3",
"assert-plus": "^1.0.0",
"bcrypt-pbkdf": "^1.0.0",
"dashdash": "^1.12.0",
"ecc-jsbn": "~0.1.1",
"getpass": "^0.1.1",
"jsbn": "~0.1.0",
"safer-buffer": "^2.0.2",
"tweetnacl": "~0.14.0"
},
"bin": {
"sshpk-conv": "bin/sshpk-conv",
"sshpk-sign": "bin/sshpk-sign",
"sshpk-verify": "bin/sshpk-verify"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/stable-hash": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
@ -20315,6 +20818,12 @@
"node": ">=0.8"
}
},
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
"license": "MIT"
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@ -20415,6 +20924,19 @@
"node": ">=6"
}
},
"node_modules/tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"license": "BSD-3-Clause",
"dependencies": {
"psl": "^1.1.28",
"punycode": "^2.1.1"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@ -20443,14 +20965,6 @@
"typescript": ">=4.8.4"
}
},
"node_modules/ts-custom-error": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz",
"integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/ts-interface-checker": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz",
@ -20493,6 +21007,24 @@
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"license": "0BSD"
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
"license": "Unlicense"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@ -20668,6 +21200,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/uniq": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
"integrity": "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==",
"license": "MIT"
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
@ -21035,7 +21573,6 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"devOptional": true,
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
@ -21113,6 +21650,20 @@
"node": ">= 0.8"
}
},
"node_modules/verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
"engines": [
"node >=0.6.0"
],
"license": "MIT",
"dependencies": {
"assert-plus": "^1.0.0",
"core-util-is": "1.0.2",
"extsprintf": "^1.2.0"
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",

View File

@ -16,7 +16,6 @@
"@iconify-json/clarity": "^1.2.1",
"@iconify-json/ic": "^1.2.1",
"@iconify-json/octicon": "^1.2.1",
"@iconify-json/tabler": "^1.2.1",
"@nuxt/eslint-config": "^1.3.0",
"@nuxt/icon": "^1.12.0",
"@nuxtjs/i18n": "^9.0.0",
@ -50,7 +49,6 @@
"@vueuse/integrations": "^11.2.0",
"@vueuse/motion": "^2.2.6",
"@vueuse/nuxt": "^11.2.0",
"@zxing/library": "^0.21.3",
"amplitude-js": "^8.21.9",
"chart.js": "^4.4.5",
"clone-deep": "^4.0.1",
@ -66,6 +64,7 @@
"pinia": "^3.0.2",
"prismjs": "^1.29.0",
"qrcode": "^1.5.4",
"quagga": "^0.12.1",
"query-builder-vue-3": "^1.0.1",
"quill": "^2.0.2",
"tailwind-merge": "^2.5.4",

10
client/plugins/feature-flags.js vendored Normal file
View File

@ -0,0 +1,10 @@
import { useFeatureFlagsStore } from '~/stores/featureFlags'
export default defineNuxtPlugin(async () => {
const featureFlagsStore = useFeatureFlagsStore()
// Load flags if they haven't been loaded yet
if (!featureFlagsStore.isLoaded) {
await featureFlagsStore.fetchFlags()
}
})

View File

@ -1,18 +1,9 @@
import { useFeatureFlagsStore } from '~/stores/featureFlags'
export default defineNuxtPlugin(async (nuxtApp) => {
// Get the pinia instance for SSR compatibility
const { $pinia } = nuxtApp
export default defineNuxtPlugin((nuxtApp) => {
const featureFlagsStore = useFeatureFlagsStore()
try {
// Pass pinia instance for SSR compatibility
const featureFlagsStore = useFeatureFlagsStore($pinia)
// Fetch flags during SSR to prevent hydration mismatches
if (!featureFlagsStore.isLoaded) {
await featureFlagsStore.fetchFlags()
}
} catch (error) {
console.error('Feature flags plugin failed:', error)
}
nuxtApp.provide('featureFlag', (key, defaultValue = false) => {
return featureFlagsStore.getFlag(key, defaultValue)
})
})

View File

@ -25,8 +25,7 @@ function mergeOptions(options) {
}
}
},
debounceWait: 300,
ignoreKeys: [] // Keys to ignore in history tracking
debounceWait: 300
}
return {
@ -35,25 +34,6 @@ function mergeOptions(options) {
}
}
/**
* Filters out ignored keys from the state object
* @param {Object} state - The state object to filter
* @param {Array} ignoreKeys - Array of keys to ignore
* @returns {Object} Filtered state object
*/
function filterState(state, ignoreKeys) {
if (!ignoreKeys || ignoreKeys.length === 0) {
return state
}
const filteredState = { ...state }
ignoreKeys.forEach(key => {
delete filteredState[key]
})
return filteredState
}
/**
* Adds undo/redo functionality to a Pinia store.
* @param {PiniaPluginContext} context - The context provided by Pinia.
@ -66,7 +46,7 @@ const PiniaHistory = (context) => {
return
}
const mergedOptions = mergeOptions(history)
const {max, persistent, persistentStrategy, ignoreKeys} = mergedOptions
const {max, persistent, persistentStrategy} = mergedOptions
const $history = reactive({
max,
@ -74,23 +54,19 @@ const PiniaHistory = (context) => {
persistentStrategy,
done: [],
undone: [],
current: JSON.stringify(filterState(store.$state, ignoreKeys)),
current: JSON.stringify(store.$state),
trigger: true,
})
const debouncedStoreUpdate = debounce((state) => {
const filteredState = filterState(state, ignoreKeys)
const currentStateHash = hash($history.current)
const newStateHash = hash(JSON.stringify(filteredState))
if (currentStateHash === newStateHash) { // Not a real change here
if (hash($history.current) === hash(JSON.stringify(state))) { // Not a real change here
return
}
if ($history.done.length >= max) $history.done.shift() // Remove oldest state if needed
$history.done.push($history.current)
$history.undone = [] // Clear redo history on new action
$history.current = JSON.stringify(filteredState)
$history.current = JSON.stringify(state)
if (persistent) {
persistentStrategy.set(store, 'undo', $history.done)
@ -114,9 +90,7 @@ const PiniaHistory = (context) => {
$history.undone.push($history.current) // Save current state for redo
$history.trigger = false
// Only patch the state that was tracked (filtered state)
const stateToRestore = JSON.parse(state)
store.$patch(stateToRestore)
store.$patch(JSON.parse(state))
nextTick(() => {
$history.current = state
$history.trigger = true
@ -140,9 +114,7 @@ const PiniaHistory = (context) => {
$history.done.push($history.current) // Save current state for undo
$history.trigger = false
// Only patch the state that was tracked (filtered state)
const stateToRestore = JSON.parse(state)
store.$patch(stateToRestore)
store.$patch(JSON.parse(state))
nextTick(() => {
$history.current = state
$history.trigger = true

View File

@ -175,42 +175,35 @@ export const useWorkingFormStore = defineStore("working_form", {
}
if (!this.content) return
const originalBlockDefinition = blocksTypes[type]
const effectiveType = originalBlockDefinition?.actual_input || type
const effectiveBlockDefinition = blocksTypes[effectiveType]
if (originalBlockDefinition?.self_hosted !== undefined && !originalBlockDefinition.self_hosted && useFeatureFlag('self_hosted')) {
const block = blocksTypes[type]
if (block?.self_hosted !== undefined && !block.self_hosted && useFeatureFlag('self_hosted')) {
useAlert().error(block?.title + ' is not allowed on self hosted. Please use our hosted version.')
return
}
if (originalBlockDefinition?.auth_required && !useAuthStore().check) {
if (block?.auth_required && !useAuthStore().check) {
useAlert().error('Please login first to add this block')
return
}
if (originalBlockDefinition?.max_count !== undefined) {
if (block?.max_count !== undefined) {
const currentCount = this.content.properties.filter(prop => prop && prop.type === type).length
if (currentCount >= originalBlockDefinition.max_count) {
useAlert().error(`Only ${originalBlockDefinition.max_count} '${originalBlockDefinition.title}' block(s) allowed per form.`)
if (currentCount >= block.max_count) {
useAlert().error(`Only ${block.max_count} '${block.title}' block(s) allowed per form.`)
return
}
openSettings = true
}
this.blockForm.type = effectiveType
this.blockForm.name = effectiveBlockDefinition?.default_block_name || 'New Block'
this.blockForm.type = type
this.blockForm.name = blocksTypes[type]?.default_block_name || 'New Block'
const newBlock = this.prefillDefault({ ...this.blockForm.data() })
newBlock.id = generateUUID()
newBlock.hidden = false
newBlock.help_position = "below_input"
// If the type was changed due to actual_input, apply original type's change settings
if (originalBlockDefinition?.actual_input && originalBlockDefinition?.type_change_settings) {
Object.assign(newBlock, originalBlockDefinition.type_change_settings)
}
if (effectiveBlockDefinition?.default_values) {
Object.assign(newBlock, effectiveBlockDefinition.default_values)
if (blocksTypes[type]?.default_values) {
Object.assign(newBlock, blocksTypes[type].default_values)
}
const insertIndex = this.determineInsertIndex(index)
@ -260,7 +253,5 @@ export const useWorkingFormStore = defineStore("working_form", {
this.setProperties(newFields)
}
},
history: {
ignoreKeys: ['structureService', 'blockForm']
}
history: {}
})

View File

@ -79,7 +79,7 @@ module.exports = {
border: 'rgba(15, 15, 15, 0.1)',
borderDark: 'rgba(255, 255, 255, 0.1)'
},
'form-color': 'rgb(from var(--form-color, var(--bg-form-color)) r g b / <alpha-value>)'
"form-color": "var(--bg-form-color)",
},
transitionProperty: {
height: "height",

View File

@ -1,10 +1,11 @@
---
services:
api:
api: &api-environment
image: jhumanj/opnform-api:latest
container_name: opnform-api
volumes:
volumes: &api-environment-volumes
- opnform_storage:/usr/share/nginx/html/storage:rw
environment:
environment: &api-env
APP_ENV: production
# Database settings
DB_HOST: db
@ -24,7 +25,7 @@ services:
db:
condition: service_healthy
redis:
condition: service_healthy
condition: service_healthy # Depend on redis being healthy too
healthcheck:
test: ["CMD-SHELL", "php /usr/share/nginx/html/artisan about || exit 1"]
interval: 30s
@ -32,49 +33,13 @@ services:
retries: 3
start_period: 60s
api-nginx:
image: nginx:alpine
container_name: opnform-api-nginx
volumes:
- ./docker/api-nginx.conf:/etc/nginx/nginx.conf:ro
- opnform_storage:/usr/share/nginx/html/storage:ro
ports:
- "127.0.0.1:7654:80" # API on port 7654
depends_on:
- api
healthcheck:
test: ["CMD-SHELL", "wget --spider -q http://localhost/ || exit 1"]
interval: 30s
timeout: 10s
retries: 3
api-worker:
image: jhumanj/opnform-api:latest
<<: *api-environment
container_name: opnform-api-worker
command: ["php", "artisan", "queue:work"]
volumes:
- opnform_storage:/usr/share/nginx/html/storage:rw
environment:
<<: *api-env
APP_ENV: production
# Database settings
DB_HOST: db
REDIS_HOST: redis
DB_DATABASE: ${DB_DATABASE:-forge}
DB_USERNAME: ${DB_USERNAME:-forge}
DB_PASSWORD: ${DB_PASSWORD:-forge}
DB_CONNECTION: ${DB_CONNECTION:-pgsql}
# PHP Configuration
PHP_MEMORY_LIMIT: "1G"
PHP_MAX_EXECUTION_TIME: "600"
PHP_UPLOAD_MAX_FILESIZE: "64M"
PHP_POST_MAX_SIZE: "64M"
env_file:
- ./api/.env
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "pgrep -f 'php artisan queue:work' > /dev/null || exit 1"]
interval: 60s
@ -83,44 +48,22 @@ services:
start_period: 30s
api-scheduler:
image: jhumanj/opnform-api:latest
<<: *api-environment
container_name: opnform-api-scheduler
command: ["php", "artisan", "schedule:work"]
volumes:
- opnform_storage:/usr/share/nginx/html/storage:rw
environment:
<<: *api-env
APP_ENV: production
# Database settings
DB_HOST: db
REDIS_HOST: redis
DB_DATABASE: ${DB_DATABASE:-forge}
DB_USERNAME: ${DB_USERNAME:-forge}
DB_PASSWORD: ${DB_PASSWORD:-forge}
DB_CONNECTION: ${DB_CONNECTION:-pgsql}
# PHP Configuration
PHP_MEMORY_LIMIT: "1G"
PHP_MAX_EXECUTION_TIME: "600"
PHP_UPLOAD_MAX_FILESIZE: "64M"
PHP_POST_MAX_SIZE: "64M"
env_file:
- ./api/.env
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "php /usr/share/nginx/html/artisan app:scheduler-status --mode=check --max-minutes=3 || exit 1"]
interval: 60s
timeout: 30s
retries: 3
start_period: 70s
start_period: 70s # Allow time for first scheduled run and cache write
ui:
image: jhumanj/opnform-client:latest
container_name: opnform-client
ports:
- "127.0.0.1:7655:3000" # UI on port 7655
env_file:
- ./client/.env
healthcheck:
@ -152,6 +95,27 @@ services:
volumes:
- postgres-data:/var/lib/postgresql/data
ingress:
image: nginx:1
container_name: opnform-ingress
volumes:
- ./docker/nginx.conf:/etc/nginx/templates/default.conf.template
ports:
- 80:80
environment:
- NGINX_MAX_BODY_SIZE=64m
depends_on:
api:
condition: service_started
ui:
condition: service_started
healthcheck:
test: ["CMD-SHELL", "nginx -t && curl -f http://localhost/ || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
volumes:
postgres-data:
opnform_storage:

View File

@ -27,9 +27,6 @@ RUN composer install --optimize-autoloader --no-interaction \
# Final stage - smaller runtime image
FROM php:8.3-fpm-alpine
# Accept version build argument
ARG APP_VERSION=unknown
# Install runtime dependencies
RUN apk add --no-cache \
libzip \
@ -78,9 +75,6 @@ RUN mkdir -p storage/framework/sessions \
# Copy the entire application from the builder stage
COPY --from=builder /app/ ./
# Set version as environment variable (more reliable than file approach)
ENV APP_VERSION_DOCKER=$APP_VERSION
# Setup entrypoint
COPY docker/php-fpm-entrypoint /usr/local/bin/opnform-entrypoint
RUN chmod a+x /usr/local/bin/*

View File

@ -1,43 +0,0 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server {
listen 80;
server_name _;
root /usr/share/nginx/html/public;
index index.php;
client_max_body_size 64M;
# Logging
access_log /dev/stdout;
error_log /dev/stderr;
# Handle all requests through PHP
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
# PHP-FPM configuration
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass opnform-api:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /usr/share/nginx/html/public/index.php;
fastcgi_param DOCUMENT_ROOT /usr/share/nginx/html/public;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_read_timeout 300;
}
# Deny access to . files
location ~ /\. {
deny all;
}
}
}

View File

@ -1,67 +0,0 @@
# Example nginx configuration for forms.portnimara.dev
# Place this in /etc/nginx/sites-available/forms.portnimara.dev
# Then create a symlink: ln -s /etc/nginx/sites-available/forms.portnimara.dev /etc/nginx/sites-enabled/
server {
listen 80;
server_name forms.portnimara.dev;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name forms.portnimara.dev;
# SSL certificates - adjust paths as needed
ssl_certificate /etc/letsencrypt/live/forms.portnimara.dev/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/forms.portnimara.dev/privkey.pem;
# SSL configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Client upload size
client_max_body_size 64M;
# Logging
access_log /var/log/nginx/forms.portnimara.dev.access.log;
error_log /var/log/nginx/forms.portnimara.dev.error.log;
# API routes - proxy to the api-nginx container
location ~ ^/(api|open|local/temp|forms/assets)/ {
proxy_pass http://127.0.0.1:7654;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
# Everything else goes to the UI container
location / {
proxy_pass http://127.0.0.1:7655;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# WebSocket support for hot reload and real-time features
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_cache_bypass $http_upgrade;
}
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
}

View File

@ -63,10 +63,7 @@ if [ "$DEV_MODE" = true ]; then
else
echo -e "${BLUE}Production environment setup complete!${NC}"
echo -e "${YELLOW}Please wait a moment for all services to start${NC}"
echo -e "${GREEN}Services are available on:${NC}"
echo -e "${GREEN}- UI: http://localhost:7655${NC}"
echo -e "${GREEN}- API: http://localhost:7654${NC}"
echo -e "${YELLOW}Note: Configure your host nginx to proxy to these ports${NC}"
echo -e "${GREEN}Then visit: http://localhost${NC}"
fi
echo -e "${BLUE}Default admin credentials:${NC}"