diff --git a/backend/app.js b/backend/app.js index 9f216c7b..1027db21 100644 --- a/backend/app.js +++ b/backend/app.js @@ -2,6 +2,7 @@ const express = require('express'); const mongoose = require('mongoose'); const cors = require('cors'); const path = require('path'); +const fs = require('fs'); const cookieParser = require('cookie-parser'); const multer = require('multer'); const passport = require('passport'); @@ -59,6 +60,25 @@ function createApp() { app.use(passport.initialize()); app.use(express.urlencoded({ extended: true })); + if (process.env.NODE_ENV !== 'production') { + app.post('/api/_agent-debug-log', (req, res) => { + try { + const payload = req.body || {}; + const line = JSON.stringify({ + hypothesisId: payload.hypothesisId || 'unknown', + location: payload.location || 'unknown', + message: payload.message || 'unknown', + data: payload.data && typeof payload.data === 'object' ? payload.data : {}, + timestamp: Number(payload.timestamp) || Date.now() + }); + fs.appendFileSync('/opt/cursor/logs/debug.log', `${line}\n`); + return res.status(200).json({ success: true }); + } catch (error) { + return res.status(500).json({ success: false, message: 'debug log write failed' }); + } + }); + } + app.use(async (req, res, next) => { try { // Debug logging to identify polling routes @@ -223,8 +243,10 @@ function createApp() { const orgRoutes = require('./routes/orgRoutes.js'); const orgRoleRoutes = require('./routes/orgRoleRoutes.js'); const orgManagementRoutes = require('./routes/orgManagementRoutes.js'); + const orgBudgetRoutes = require('./routes/orgBudgetRoutes.js'); const orgInviteRoutes = require('./routes/orgInviteRoutes.js'); const orgMessageRoutes = require('./routes/orgMessageRoutes.js'); + const taskManagementRoutes = require('./routes/taskManagementRoutes.js'); const roomRoutes = require('./routes/roomRoutes.js'); const adminRoutes = require('./routes/adminRoutes.js'); const eventsRoutes = require('./events/index.js'); @@ -258,9 +280,11 @@ function createApp() { app.use(orgRoutes); app.use('/org-roles', orgRoleRoutes); app.use('/org-management', orgManagementRoutes); + app.use('/org-budgets', orgBudgetRoutes); app.use('/org-invites', orgInviteRoutes); app.use('/org-messages', orgMessageRoutes); app.use('/org-event-management', orgEventManagementRoutes); + app.use('/org-event-management', taskManagementRoutes); app.use('/admin', roomRoutes); app.use(adminRoutes); app.use(formRoutes); @@ -304,6 +328,8 @@ function createApp() { }; try { + const getModels = require('./services/getModelService'); + const { Classroom } = getModels(req, 'Classroom'); // Upload image to S3 const s3Response = await s3.upload(s3Params).promise(); const imageUrl = s3Response.Location; @@ -313,7 +339,7 @@ function createApp() { { name: classroomName }, { image: imageUrl }, { new: true, upsert: true } - ); + ).populate('building', 'name'); res.status(200).json({ message: 'Image uploaded and classroom updated.', classroom }); } catch (error) { diff --git a/backend/constants/permissions.js b/backend/constants/permissions.js index 5ee4a224..690c96ce 100644 --- a/backend/constants/permissions.js +++ b/backend/constants/permissions.js @@ -25,7 +25,10 @@ const ORG_PERMISSIONS = { // Advanced features MANAGE_INTEGRATIONS: 'manage_integrations', // Can manage third-party integrations - ACCESS_ADVANCED_FEATURES: 'access_advanced_features' // Can access beta/advanced features + ACCESS_ADVANCED_FEATURES: 'access_advanced_features', // Can access beta/advanced features + + /** Atlas CMS Phase 1: upload/version governance documents (constitution, etc.) */ + MANAGE_GOVERNANCE: 'manage_governance' }; // Event-specific permissions diff --git a/backend/middlewares/verifyToken.js b/backend/middlewares/verifyToken.js index 374b26bf..1920e284 100644 --- a/backend/middlewares/verifyToken.js +++ b/backend/middlewares/verifyToken.js @@ -50,6 +50,21 @@ const verifyToken = async (req, res, next) => { try { const decodedToken = jwt.verify(token, process.env.JWT_SECRET); await resolveRequestUser(req, decodedToken); + if (req.user?.userId) { + try { + const { User } = getModels(req, 'User'); + const accountUser = await User.findById(req.user.userId).select('accessSuspended').lean(); + if (accountUser?.accessSuspended) { + return res.status(403).json({ + success: false, + message: 'This account has been suspended.', + code: 'ACCOUNT_SUSPENDED', + }); + } + } catch (checkErr) { + console.error('[verifyToken] accessSuspended check failed:', checkErr); + } + } return next(); } catch (err) { if (err.name === 'TokenExpiredError') { diff --git a/backend/migrations/migrateClassroomBuildingRefs.js b/backend/migrations/migrateClassroomBuildingRefs.js new file mode 100644 index 00000000..c3235ce3 --- /dev/null +++ b/backend/migrations/migrateClassroomBuildingRefs.js @@ -0,0 +1,152 @@ +/** + * One-shot migration: legacy Classroom.building (string) → ObjectId ref to Building. + * Uses the native driver on collection names so it stays reliable across schema changes. + * + * Deploy note: existing tenants still have string `building` values until this runs. + * With the ObjectId classroom schema, hydrate those docs before migration will throw CastError. + * Prefer running once via CLI *before* restarting app servers on this version: + * MIGRATION_SCHOOL=rpi node Meridian/backend/migrations/migrateClassroomBuildingRefs.js + * Optional: FORCE=1 to clear the per-tenant guard row and re-run. + */ + +const mongoose = require('mongoose'); +const { connectToDatabase } = require('../connectionsManager'); + +const MIGRATION_KEY = 'classroom_building_oid_ref'; +const CLASSROOMS_COLL = 'classrooms1'; +const BUILDINGS_COLL = 'buildings'; +const RUNS_COLL = 'admin_migration_runs'; + +const DEFAULT_BUILDING_IMAGE = '/classrooms/default.png'; +const DEFAULT_TIME = { start: 0, end: 24 * 60 }; + +function escapeRegex(s) { + return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +async function findBuildingByName(buildings, name) { + const trimmed = String(name || '').trim(); + if (!trimmed) return null; + let doc = await buildings.findOne({ name: trimmed }); + if (doc) return doc; + doc = await buildings.findOne({ name: new RegExp(`^${escapeRegex(trimmed)}$`, 'i') }); + return doc; +} + +/** + * @param {import('mongoose').Connection} mongooseConn + * @param {{ force?: boolean }} [options] + */ +async function runMigrateClassroomBuildingRefs(mongooseConn, options = {}) { + const { force = false } = options; + const db = mongooseConn.db; + const runs = db.collection(RUNS_COLL); + const classrooms = db.collection(CLASSROOMS_COLL); + const buildings = db.collection(BUILDINGS_COLL); + + if (!force) { + const prior = await runs.findOne({ key: MIGRATION_KEY }); + if (prior) { + return { + skipped: true, + reason: 'already_run', + ranAt: prior.completedAt || null, + }; + } + } else { + await runs.deleteMany({ key: MIGRATION_KEY }); + } + + const stringRooms = await classrooms + .find({ + building: { $exists: true, $type: 'string', $nin: ['', null] }, + }) + .toArray(); + + const distinctNames = [ + ...new Set(stringRooms.map((d) => String(d.building).trim()).filter(Boolean)), + ]; + + const buildingsCreated = []; + const buildingsReused = []; + const nameToId = new Map(); + + for (const name of distinctNames) { + const existing = await findBuildingByName(buildings, name); + if (existing) { + nameToId.set(name, existing._id); + buildingsReused.push(name); + continue; + } + const ins = await buildings.insertOne({ + name, + image: DEFAULT_BUILDING_IMAGE, + time: DEFAULT_TIME, + }); + nameToId.set(name, ins.insertedId); + buildingsCreated.push(name); + } + + const bulkOps = stringRooms + .map((doc) => { + const name = String(doc.building).trim(); + const bid = nameToId.get(name); + if (!bid) return null; + return { + updateOne: { + filter: { _id: doc._id, building: { $type: 'string' } }, + update: { $set: { building: bid } }, + }, + }; + }) + .filter(Boolean); + + let classroomsUpdated = 0; + if (bulkOps.length) { + const wr = await classrooms.bulkWrite(bulkOps, { ordered: false }); + classroomsUpdated = wr.modifiedCount || 0; + } + + await classrooms.updateMany({ building: '' }, { $unset: { building: '' } }); + + const summary = { + skipped: false, + distinctBuildingNames: distinctNames.length, + buildingsCreatedCount: buildingsCreated.length, + buildingsReusedCount: buildingsReused.length, + buildingsCreated, + classroomsUpdated, + }; + + await runs.insertOne({ + key: MIGRATION_KEY, + completedAt: new Date(), + summary, + }); + + return summary; +} + +module.exports = { + runMigrateClassroomBuildingRefs, + MIGRATION_KEY, +}; + +async function cliMain() { + const school = process.env.MIGRATION_SCHOOL || process.argv[2] || 'rpi'; + const conn = await connectToDatabase(school); + try { + const out = await runMigrateClassroomBuildingRefs(conn, { force: process.env.FORCE === '1' }); + console.log(JSON.stringify(out, null, 2)); + } finally { + await conn.close().catch(() => {}); + await mongoose.disconnect().catch(() => {}); + } +} + +if (require.main === module) { + cliMain().then(() => process.exit(0)).catch((e) => { + console.error(e); + process.exit(1); + }); +} diff --git a/backend/migrations/seedDomainSpaceGovernance.js b/backend/migrations/seedDomainSpaceGovernance.js new file mode 100644 index 00000000..763a6349 --- /dev/null +++ b/backend/migrations/seedDomainSpaceGovernance.js @@ -0,0 +1,59 @@ +#!/usr/bin/env node +require('dotenv').config(); + +const mongoose = require('mongoose'); +const { connectToDatabase } = require('../connectionsManager'); +const getModels = require('../services/getModelService'); + +function getDefaultSpaceGovernance() { + return { + governingScope: { + kind: 'all_spaces', + buildingIds: [], + spaceIds: [], + spaceGroupIds: [] + }, + concernScope: { + kind: 'campus_wide', + buildingIds: [], + spaceIds: [], + spaceGroupIds: [] + }, + scopeMode: 'inclusive', + priorityRules: [] + }; +} + +async function run() { + const school = process.env.MIGRATION_SCHOOL || process.argv[2] || 'rpi'; + const db = await connectToDatabase(school); + const req = { db }; + const { Domain } = getModels(req, 'Domain'); + + const result = await Domain.updateMany( + { + $or: [ + { spaceGovernance: { $exists: false } }, + { spaceGovernance: null } + ] + }, + { + $set: { + spaceGovernance: getDefaultSpaceGovernance() + } + } + ); + + console.log( + `[seedDomainSpaceGovernance] school=${school} matched=${result.matchedCount || 0} modified=${result.modifiedCount || 0}` + ); +} + +run() + .catch((error) => { + console.error('[seedDomainSpaceGovernance] failed', error); + process.exitCode = 1; + }) + .finally(async () => { + await mongoose.disconnect(); + }); diff --git a/backend/routes/adminRoutes.js b/backend/routes/adminRoutes.js index e96a7e50..15824956 100644 --- a/backend/routes/adminRoutes.js +++ b/backend/routes/adminRoutes.js @@ -5,7 +5,7 @@ const router = express.Router(); const { verifyToken, authorizeRoles } = require('../middlewares/verifyToken'); const { requireAdmin } = require('../middlewares/requireAdmin'); const mongoose = require('mongoose'); -const { connectToDatabase, connectToGlobalDatabase } = require('../connectionsManager'); +const { connectToDatabase } = require('../connectionsManager'); const { getConnections, disconnectSocket, disconnectAll } = require('../socket'); const { createSession } = require('../utilities/sessionUtils'); const { getCookieDomain } = require('../utilities/cookieUtils'); @@ -342,99 +342,19 @@ router.post('/admin/platform-admins', verifyToken, requireAdmin, async (req, res }); /** - * POST /admin/migrate-users-to-global-identity – backfill GlobalUser + TenantMembership for existing tenant Users. - * Alternative to running scripts/migrateUsersToGlobalIdentity.js when shell access is unavailable. - * Idempotent. Body: { tenantKeys?: string[] } (optional; defaults to rpi,tvcog). - * Restricted to platform admins only. + * POST /admin/migrate-classroom-building-refs + * One-shot (per tenant DB): create Building docs from distinct legacy Classroom.building strings, + * then replace those strings with ObjectId refs. Guarded by admin_migration_runs; optional body.force to re-run. */ -router.post('/admin/migrate-users-to-global-identity', verifyToken, requireAdmin, async (req, res) => { - if (!req.user.platformRoles?.includes('platform_admin')) { - return res.status(403).json({ success: false, message: 'Platform admin required.' }); - } +router.post('/admin/migrate-classroom-building-refs', verifyToken, requireAdmin, async (req, res) => { try { - const tenantKeys = Array.isArray(req.body?.tenantKeys) && req.body.tenantKeys.length > 0 - ? req.body.tenantKeys.map((k) => String(k).trim().toLowerCase()).filter(Boolean) - : ['rpi', 'tvcog']; - - const userSchema = require('../schemas/user'); - const globalUserSchema = require('../schemas/globalUser'); - const tenantMembershipSchema = require('../schemas/tenantMembership'); - - const globalDb = await connectToGlobalDatabase(); - const GlobalUser = globalDb.model('GlobalUser', globalUserSchema, 'global_users'); - const TenantMembership = globalDb.model('TenantMembership', tenantMembershipSchema, 'tenant_memberships'); - - const summary = { globalUsersCreated: 0, membershipsCreated: 0, tenants: {} }; - - for (const tenantKey of tenantKeys) { - summary.tenants[tenantKey] = { usersProcessed: 0, globalUsersCreated: 0, membershipsCreated: 0 }; - const db = await connectToDatabase(tenantKey); - const User = db.model('User', userSchema, 'users'); - const users = await User.find({}).lean(); - - for (const user of users) { - const email = (user.email || '').trim().toLowerCase(); - if (!email) continue; - - summary.tenants[tenantKey].usersProcessed++; - - const source = { - email: user.email, - name: user.name, - picture: user.picture, - googleId: user.googleId, - appleId: user.appleId, - samlId: user.samlId, - samlProvider: user.samlProvider, - }; - - let globalUser = await GlobalUser.findOne({ email }); - if (!globalUser) { - const providerQuery = { $or: [{ email }] }; - if (source.googleId) providerQuery.$or.push({ googleId: source.googleId }); - if (source.appleId) providerQuery.$or.push({ appleId: source.appleId }); - if (source.samlId && source.samlProvider) { - providerQuery.$or.push({ samlId: source.samlId, samlProvider: source.samlProvider }); - } - globalUser = await GlobalUser.findOne(providerQuery); - } - if (!globalUser) { - globalUser = new GlobalUser({ - email, - name: source.name || '', - picture: source.picture || '', - googleId: source.googleId, - appleId: source.appleId, - samlId: source.samlId, - samlProvider: source.samlProvider, - }); - await globalUser.save(); - summary.globalUsersCreated++; - summary.tenants[tenantKey].globalUsersCreated++; - } - - let membership = await TenantMembership.findOne({ - globalUserId: globalUser._id, - tenantKey, - }); - if (!membership) { - membership = new TenantMembership({ - globalUserId: globalUser._id, - tenantKey, - tenantUserId: user._id, - status: 'active', - }); - await membership.save(); - summary.membershipsCreated++; - summary.tenants[tenantKey].membershipsCreated++; - } - } - } - - console.log('POST /admin/migrate-users-to-global-identity completed:', summary); - res.json({ success: true, data: summary }); + const { runMigrateClassroomBuildingRefs } = require('../migrations/migrateClassroomBuildingRefs'); + const force = Boolean(req.body?.force); + const data = await runMigrateClassroomBuildingRefs(req.db, { force }); + console.log('POST /admin/migrate-classroom-building-refs completed:', data); + res.json({ success: true, data }); } catch (err) { - console.error('POST /admin/migrate-users-to-global-identity failed:', err); + console.error('POST /admin/migrate-classroom-building-refs failed:', err); res.status(500).json({ success: false, message: err.message }); } }); diff --git a/backend/routes/authRoutes.js b/backend/routes/authRoutes.js index a85f8d61..4134c0f9 100644 --- a/backend/routes/authRoutes.js +++ b/backend/routes/authRoutes.js @@ -84,6 +84,16 @@ async function getCurrentTenantAdminUser(req) { } async function completeLoginWithAdminMfa(req, res, globalUser, tenantUser, platformRoles, message) { + if (tenantUser?.accessSuspended) { + return { + status: 403, + body: { + success: false, + message: 'This account has been suspended.', + code: 'ACCOUNT_SUSPENDED', + }, + }; + } const isAdmin = isAdminLevelAccount(tenantUser, platformRoles); const mfaStatus = getMfaStatus(tenantUser); @@ -356,6 +366,14 @@ router.post('/refresh-token', async (req, res) => { const { user, globalUser } = validation; const isMobile = isMobileClient(req); + if (user?.accessSuspended) { + return res.status(403).json({ + success: false, + message: 'This account has been suspended.', + code: 'ACCOUNT_SUSPENDED', + }); + } + if (globalUser) { const platformRoles = await authGlobalService.getPlatformRolesForGlobalUser(req, globalUser._id); const tokens = await authGlobalService.issueTokens( @@ -512,6 +530,37 @@ router.get('/validate-token', verifyToken, async (req, res) => { // Fetch pending org invites for this user const pendingOrgInvites = await orgInviteService.getPendingForUser(req); + // Domains where this user is an active member of a stakeholder role (domain dashboard entry points) + try { + const { StakeholderRole, Domain } = getModels(req, 'StakeholderRole', 'Domain'); + const roles = await StakeholderRole.find({ + members: { + $elemMatch: { + userId: user._id, + isActive: { $ne: false } + } + }, + isActive: true + }) + .select('domainId') + .lean(); + + const rawDomainIds = [...new Set((roles || []).map((r) => r.domainId).filter(Boolean))]; + const domainDocs = + rawDomainIds.length > 0 + ? await Domain.find({ _id: { $in: rawDomainIds }, isActive: true }).select('name').lean() + : []; + const nameById = new Map(domainDocs.map((d) => [String(d._id), d.name])); + + user.stakeholderDomainDashboards = rawDomainIds.map((id) => ({ + domainId: String(id), + domainName: nameById.get(String(id)) || null + })); + } catch (e) { + console.warn('validate-token: stakeholder domain dashboards skipped', e?.message || e); + user.stakeholderDomainDashboards = []; + } + console.log(`GET: /validate-token token is valid for user ${user.username}`) const data = { user: user, diff --git a/backend/routes/dataRoutes.js b/backend/routes/dataRoutes.js index 280be065..07fe334d 100644 --- a/backend/routes/dataRoutes.js +++ b/backend/routes/dataRoutes.js @@ -12,11 +12,39 @@ const path = require('path'); const mongoose = require('mongoose'); const { clean } = require('../services/profanityFilterService'); const getModels = require('../services/getModelService'); +const { classroomBuildingName } = require('../utilities/classroomBuildingName'); const router = express.Router(); +// Lightweight space availability summary for tenant-aware UI decisions +router.get('/spaces-summary', async (req, res) => { + try { + const { Classroom, Building } = getModels(req, 'Classroom', 'Building'); + const [roomsCount, buildingsCount] = await Promise.all([ + Classroom.countDocuments({}), + Building.countDocuments({}), + ]); + + res.json({ + success: true, + data: { + roomsCount, + buildingsCount, + hasRoomsOrBuildings: roomsCount > 0 || buildingsCount > 0, + }, + }); + } catch (error) { + console.error('GET /spaces-summary failed', error); + res.status(500).json({ + success: false, + message: 'Failed to load spaces summary', + error: error.message, + }); + } +}); + // Route to get featured content (rooms and events) for explore screen router.get('/featured-all', async (req, res) => { try { @@ -25,6 +53,22 @@ router.get('/featured-all', async (req, res) => { // Get 5 random rooms const rooms = await Classroom.aggregate([ { $sample: { size: 5 } }, + { + $lookup: { + from: 'buildings', + localField: 'building', + foreignField: '_id', + as: '__buildingDoc', + }, + }, + { + $addFields: { + building: { + $ifNull: [{ $arrayElemAt: ['$__buildingDoc.name', 0] }, ''], + }, + }, + }, + { $project: { __buildingDoc: 0 } }, { $project: { _id: 1, @@ -35,9 +79,9 @@ router.get('/featured-all', async (req, res) => { image: 1, attributes: 1, average_rating: 1, - number_of_ratings: 1 - } - } + number_of_ratings: 1, + }, + }, ]); // Get 5 random events @@ -575,7 +619,7 @@ router.get('/getroom/:id', async (req, res) => { } // Find the classroom and schedule - const room = await Classroom.findOne({ _id: roomId }); + const room = await Classroom.findOne({ _id: roomId }).populate('building', 'name'); const schedule = await Schedule.findOne({ classroom_id: roomId }); console.log(`GET: /getroom/${roomId}`); if (room) { @@ -627,6 +671,7 @@ router.get('/top-rated-rooms', async (req, res) => { }) .limit(parseInt(limit)) .select('_id name image building floor capacity attributes average_rating number_of_ratings') + .populate('building', 'name') .lean(); // Get room IDs to fetch schedules @@ -648,7 +693,7 @@ router.get('/top-rated-rooms', async (req, res) => { id: room._id.toString(), name: room.name || 'Unknown Room', image: room.image || null, - building: room.building || '', + building: classroomBuildingName(room), floor: room.floor || '', capacity: room.capacity || 0, attributes: room.attributes || [], @@ -901,7 +946,11 @@ router.post('/getbatch-new', async (req, res) => { // Fetch schedules and populate the referenced classrooms let schedules = await Schedule.find({ classroom_id: { $in: queryIds } }) - .populate(exhaustive ? { path: 'classroom_id', model: 'Classroom' } : '') + .populate( + exhaustive + ? { path: 'classroom_id', model: 'Classroom', populate: { path: 'building', select: 'name' } } + : '' + ) .lean(); // Create a mapping from classroom_id to schedule data @@ -989,7 +1038,10 @@ router.get('/get-recommendation', verifyTokenOptional, async (req, res) => { if (randomClassroom && randomClassroom.length > 0) { randomClassroom = randomClassroom[0]; - randomClassroom = await Classroom.findOne({ _id: randomClassroom.classroom_id }); + randomClassroom = await Classroom.findOne({ _id: randomClassroom.classroom_id }).populate( + 'building', + 'name' + ); console.log(`GET: /get-recommendation/${userId}`); return res.status(200).json({ success: true, message: 'Recommendation found', data: randomClassroom }); } @@ -1014,7 +1066,10 @@ router.get('/get-recommendation', verifyTokenOptional, async (req, res) => { if (randomClassroom && randomClassroom.length > 0) { randomClassroom = randomClassroom[0]; - randomClassroom = await Classroom.findOne({ _id: randomClassroom.classroom_id }); + randomClassroom = await Classroom.findOne({ _id: randomClassroom.classroom_id }).populate( + 'building', + 'name' + ); console.log(`GET: /get-recommendation`); return res.status(200).json({ success: true, message: 'Recommendation found', data: randomClassroom }); } diff --git a/backend/routes/orgBudgetRoutes.js b/backend/routes/orgBudgetRoutes.js new file mode 100644 index 00000000..af177965 --- /dev/null +++ b/backend/routes/orgBudgetRoutes.js @@ -0,0 +1,232 @@ +const express = require('express'); +const getModels = require('../services/getModelService'); +const orgBudgetSchema = require('../schemas/orgBudget'); +const { verifyToken } = require('../middlewares/verifyToken'); +const { requireOrgPermission } = require('../middlewares/orgPermissions'); +const { ORG_PERMISSIONS } = require('../constants/permissions'); +const budgetService = require('../services/budgetService'); + +const router = express.Router(); + +/** Stale unique indexes on org_id/name (pre-schema) collide: all docs lack those fields → duplicate null keys. */ +const orgBudgetIndexesSynced = new WeakSet(); + +router.use(async (req, res, next) => { + try { + if (!req.db || orgBudgetIndexesSynced.has(req.db)) return next(); + const OrgBudget = req.db.models.OrgBudget || req.db.model('OrgBudget', orgBudgetSchema, 'orgBudgets'); + await OrgBudget.syncIndexes(); + orgBudgetIndexesSynced.add(req.db); + } catch (e) { + console.warn('[org-budgets] OrgBudget.syncIndexes:', e.message); + } + next(); +}); + +router.get('/:orgId/budget-templates', verifyToken, requireOrgPermission(ORG_PERMISSIONS.VIEW_FINANCES), async (req, res) => { + try { + const { orgId } = req.params; + const { Org } = getModels(req, 'Org'); + const org = await Org.findById(orgId).select('orgTypeKey org_name').lean(); + if (!org) { + return res.status(404).json({ success: false, message: 'Organization not found' }); + } + const config = await budgetService.ensureFinanceConfig(req); + const lineItemPolicy = await budgetService.getBudgetLineItemPolicy(req); + res.status(200).json({ + success: true, + data: { + templates: config.budgetTemplates || [], + workflowPresets: config.workflowPresets || [], + orgTypeKey: org.orgTypeKey || 'default', + lineItemPolicy + } + }); + } catch (e) { + console.error('budget-templates', e); + res.status(500).json({ success: false, message: e.message }); + } +}); + +router.get('/:orgId/budgets', verifyToken, requireOrgPermission(ORG_PERMISSIONS.VIEW_FINANCES), async (req, res) => { + try { + const { orgId } = req.params; + const data = await budgetService.listBudgetsForOrg(req, orgId); + res.status(200).json({ success: true, data }); + } catch (e) { + console.error('list budgets', e); + res.status(500).json({ success: false, message: e.message }); + } +}); + +router.get( + '/:orgId/budgets/:budgetId', + verifyToken, + requireOrgPermission(ORG_PERMISSIONS.VIEW_FINANCES), + async (req, res) => { + try { + const { orgId, budgetId } = req.params; + const data = await budgetService.getBudgetById(req, orgId, budgetId); + if (!data) { + return res.status(404).json({ success: false, message: 'Budget not found' }); + } + res.status(200).json({ success: true, data }); + } catch (e) { + console.error('get budget', e); + res.status(500).json({ success: false, message: e.message }); + } + } +); + +router.post('/:orgId/budgets', verifyToken, requireOrgPermission(ORG_PERMISSIONS.MANAGE_FINANCES), async (req, res) => { + try { + const { orgId } = req.params; + const data = await budgetService.createBudget(req, orgId, req.user.userId, req.body || {}); + res.status(201).json({ success: true, data }); + } catch (e) { + const code = e.statusCode || 500; + console.error('create budget', e); + res.status(code).json({ success: false, message: e.message }); + } +}); + +router.patch( + '/:orgId/budgets/:budgetId', + verifyToken, + requireOrgPermission(ORG_PERMISSIONS.MANAGE_FINANCES), + async (req, res) => { + try { + const { orgId, budgetId } = req.params; + const data = await budgetService.updateBudgetDraft(req, orgId, budgetId, req.user.userId, req.body || {}); + res.status(200).json({ success: true, data }); + } catch (e) { + const code = e.statusCode || 500; + console.error('update budget', e); + res.status(code).json({ success: false, message: e.message }); + } + } +); + +router.post( + '/:orgId/budgets/:budgetId/submit', + verifyToken, + requireOrgPermission(ORG_PERMISSIONS.MANAGE_FINANCES), + async (req, res) => { + try { + const { orgId, budgetId } = req.params; + const data = await budgetService.submitBudget(req, orgId, budgetId, req.user.userId); + res.status(200).json({ success: true, data }); + } catch (e) { + const code = e.statusCode || 500; + console.error('submit budget', e); + res.status(code).json({ success: false, message: e.message }); + } + } +); + +router.post( + '/:orgId/budgets/:budgetId/comments', + verifyToken, + requireOrgPermission(ORG_PERMISSIONS.MANAGE_FINANCES), + async (req, res) => { + try { + const { orgId, budgetId } = req.params; + const data = await budgetService.addComment(req, orgId, budgetId, req.user.userId, req.body || {}); + res.status(200).json({ success: true, data }); + } catch (e) { + const code = e.statusCode || 500; + console.error('budget comment', e); + res.status(code).json({ success: false, message: e.message }); + } + } +); + +router.put( + '/:orgId/budgets/:budgetId/stages/:stageKey/approve', + verifyToken, + requireOrgPermission(ORG_PERMISSIONS.MANAGE_FINANCES), + async (req, res) => { + try { + const { orgId, budgetId, stageKey } = req.params; + const data = await budgetService.approveStageOrg(req, orgId, budgetId, req.user.userId, stageKey); + res.status(200).json({ success: true, data }); + } catch (e) { + const code = e.statusCode || 500; + console.error('approve org stage', e); + res.status(code).json({ success: false, message: e.message }); + } + } +); + +router.put( + '/:orgId/budgets/:budgetId/stages/:stageKey/reject', + verifyToken, + requireOrgPermission(ORG_PERMISSIONS.MANAGE_FINANCES), + async (req, res) => { + try { + const { orgId, budgetId, stageKey } = req.params; + const data = await budgetService.rejectBudget( + req, + orgId, + budgetId, + req.user.userId, + { ...(req.body || {}), stageKey }, + { platformOnly: false } + ); + res.status(200).json({ success: true, data }); + } catch (e) { + const code = e.statusCode || 500; + console.error('reject org stage', e); + res.status(code).json({ success: false, message: e.message }); + } + } +); + +router.put( + '/:orgId/budgets/:budgetId/stages/:stageKey/request-revision', + verifyToken, + requireOrgPermission(ORG_PERMISSIONS.MANAGE_FINANCES), + async (req, res) => { + try { + const { orgId, budgetId, stageKey } = req.params; + const data = await budgetService.requestRevision( + req, + orgId, + budgetId, + req.user.userId, + { ...(req.body || {}), stageKey }, + { platformOnly: false } + ); + res.status(200).json({ success: true, data }); + } catch (e) { + const code = e.statusCode || 500; + console.error('request revision org', e); + res.status(code).json({ success: false, message: e.message }); + } + } +); + +router.get( + '/:orgId/budgets/:budgetId/export', + verifyToken, + requireOrgPermission(ORG_PERMISSIONS.VIEW_FINANCES), + async (req, res) => { + try { + const { orgId, budgetId } = req.params; + const format = (req.query.format || 'json').toLowerCase(); + const out = await budgetService.exportBudget(req, orgId, budgetId, format); + if (format === 'csv') { + res.setHeader('Content-Type', out.contentType); + res.setHeader('Content-Disposition', `attachment; filename="${out.filename}"`); + return res.status(200).send(out.body); + } + return res.status(200).json(out.body); + } catch (e) { + const code = e.statusCode || 500; + console.error('export budget', e); + res.status(code).json({ success: false, message: e.message }); + } + } +); + +module.exports = router; diff --git a/backend/routes/orgEventManagementRoutes.js b/backend/routes/orgEventManagementRoutes.js index 01e49915..a5d3bd5a 100644 --- a/backend/routes/orgEventManagementRoutes.js +++ b/backend/routes/orgEventManagementRoutes.js @@ -4,7 +4,11 @@ const { verifyToken } = require('../middlewares/verifyToken'); const getModels = require('../services/getModelService'); const { requireEventManagement, requireOrgPermission } = require('../middlewares/orgPermissions'); const StudySessionService = require('../services/studySessionService'); +const ResourceReservationService = require('../services/resourceReservationService'); +const ReservationMetricsService = require('../services/reservationMetricsService'); +const ExternalRoomSyncService = require('../services/externalRoomSyncService'); const { resolveAnonymousEmail, resolveAnonymousName } = require('../services/eventAnnouncementService'); +const { getEffectivePolicy, assertOrgAllowsEventCreation, assertEventReservationReady } = require('../services/atlasPolicyService'); const buildOrgEventScope = (orgId) => ([ { hostingId: orgId }, @@ -19,6 +23,44 @@ const buildScopedEventQuery = (orgId, eventId) => ({ }); const getHostOrgIdFromEvent = (event) => String(event?.hostingId?._id || event?.hostingId || ''); +const RESERVATION_PREFLIGHT_ENFORCED = process.env.ENFORCE_RESOURCE_PREFLIGHT !== 'false'; +const RESERVATION_OPS_PHASE2_ENABLED = process.env.RESERVATION_OPS_PHASE2 !== 'false'; + +async function runReservationPreflight(req, event, options = {}) { + const resourceId = options.resourceId || event?.reservation?.resourceId || event?.classroom_id || null; + if (!resourceId) return { ok: true, availability: { isAvailable: true, reason: 'No reservable resource linked' } }; + const reservationService = new ResourceReservationService(req); + const availability = await reservationService.applyAvailabilitySnapshot(event, { + startTime: options.startTime || event.start_time, + endTime: options.endTime || event.end_time, + resourceId, + excludeEventId: options.excludeEventId || event._id + }); + if (!availability.isAvailable && RESERVATION_PREFLIGHT_ENFORCED) { + console.info('[reservation_preflight]', { + route: 'org-event-management', + eventId: String(event?._id || ''), + resourceId: String(resourceId), + outcome: 'blocked', + reason: availability.reason || 'unknown' + }); + return { ok: false, code: 'reservation_conflict', availability }; + } + console.info('[reservation_preflight]', { + route: 'org-event-management', + eventId: String(event?._id || ''), + resourceId: String(resourceId), + outcome: availability.isAvailable ? 'allowed' : 'advisory_conflict', + reason: availability.reason || '' + }); + return { ok: true, availability }; +} + +function reservationOpsDisabled(res) { + if (RESERVATION_OPS_PHASE2_ENABLED) return false; + res.status(404).json({ success: false, message: 'Reservation operations are disabled' }); + return true; +} // ==================== ORGANIZATION EVENT ANALYTICS ==================== @@ -1367,6 +1409,17 @@ router.post('/:orgId/events/from-template/:templateId', verifyToken, requireEven }); } + const { Org } = getModels(req, 'Org'); + const hostOrg = await Org.findById(orgId); + if (!hostOrg) { + return res.status(404).json({ success: false, message: 'Organization not found' }); + } + const atlasPolicy = await getEffectivePolicy(req); + const createCheck = assertOrgAllowsEventCreation(atlasPolicy, hostOrg); + if (!createCheck.ok) { + return res.status(403).json({ success: false, message: createCheck.message }); + } + // Create event from template const eventData = { ...template.templateData, @@ -1379,6 +1432,28 @@ router.post('/:orgId/events/from-template/:templateId', verifyToken, requireEven }; const event = new Event(eventData); + const reservationService = new ResourceReservationService(req); + const resourceId = customizations.resourceId || event.classroom_id || null; + event.reservation = reservationService.normalizeEventReservation({ + classroom_id: resourceId, + status: event.status + }); + if (resourceId) { + event.classroom_id = resourceId; + } + const reservationPreflight = await runReservationPreflight(req, event, { + resourceId: event.classroom_id, + startTime: event.start_time, + endTime: event.end_time + }); + if (!reservationPreflight.ok) { + return res.status(409).json({ + success: false, + code: reservationPreflight.code, + message: reservationPreflight.availability.reason || 'Resource is unavailable for the requested time', + conflicts: reservationPreflight.availability.conflicts || [] + }); + } await event.save(); // Create default EventAgenda for the new event @@ -1454,6 +1529,41 @@ router.put('/:orgId/events/:eventId', verifyToken, requireEventManagement('orgId } }); + if (updateData.resourceId !== undefined) { + event.classroom_id = updateData.resourceId || null; + } + if (event.classroom_id || event.reservation?.resourceId) { + const reservationService = new ResourceReservationService(req); + event.reservation = reservationService.normalizeEventReservation(event); + if (event.classroom_id && !event.reservation.resourceId) { + event.reservation.resourceId = event.classroom_id; + } + const reservationPreflight = await runReservationPreflight(req, event, { + resourceId: event.reservation.resourceId || event.classroom_id, + startTime: event.start_time, + endTime: event.end_time, + excludeEventId: event._id + }); + if (!reservationPreflight.ok) { + return res.status(409).json({ + success: false, + code: reservationPreflight.code, + message: reservationPreflight.availability.reason || 'Resource is unavailable for the requested time', + conflicts: reservationPreflight.availability.conflicts || [] + }); + } + const reservationReady = assertEventReservationReady(event, { + allowedStates: ['requested', 'approved', 'hold'] + }); + if (!reservationReady.ok) { + return res.status(409).json({ + success: false, + code: reservationReady.code || 'EVENT_RESERVATION_NOT_READY', + message: reservationReady.message + }); + } + } + await event.save(); const hostOrgId = getHostOrgIdFromEvent(event); @@ -1909,23 +2019,29 @@ router.post('/:orgId/events/:eventId/agenda/publish', verifyToken, requireEventM }); } - // Only check room availability if event has a classroom_id - if (event.classroom_id) { - const studySessionService = new StudySessionService(req); - const availability = await studySessionService.checkRoomAvailabilityByClassroomId( - event.start_time, - new Date(newEndTime), - event.classroom_id, - eventId - ); - - if (!availability.isAvailable) { - return res.status(409).json({ - success: false, - message: availability.reason || 'Room is unavailable for the requested time', - conflicts: availability.conflicts - }); - } + const reservationPreflight = await runReservationPreflight(req, event, { + resourceId: event?.reservation?.resourceId || event.classroom_id, + startTime: event.start_time, + endTime: new Date(newEndTime), + excludeEventId: eventId + }); + if (!reservationPreflight.ok) { + return res.status(409).json({ + success: false, + code: reservationPreflight.code, + message: reservationPreflight.availability.reason || 'Resource is unavailable for the requested time', + conflicts: reservationPreflight.availability.conflicts || [] + }); + } + const reservationReady = assertEventReservationReady(event, { + allowedStates: ['requested', 'approved', 'hold'] + }); + if (!reservationReady.ok) { + return res.status(409).json({ + success: false, + code: reservationReady.code || 'EVENT_RESERVATION_NOT_READY', + message: reservationReady.message + }); } // Update event end_time @@ -3265,4 +3381,79 @@ router.post('/:orgId/events/:eventId/export', verifyToken, requireEventManagemen } }); +router.get('/:orgId/reservations/conflicts', verifyToken, requireEventManagement('orgId'), async (req, res) => { + if (reservationOpsDisabled(res)) return; + try { + const { orgId } = req.params; + const { limit = 50 } = req.query; + const service = new ResourceReservationService(req); + const conflicts = await service.listUnresolvedConflicts({ orgId, limit: Number(limit) || 50 }); + return res.status(200).json({ success: true, data: conflicts }); + } catch (error) { + return res.status(500).json({ success: false, message: error.message }); + } +}); + +router.patch('/:orgId/reservations/:eventId/exception', verifyToken, requireEventManagement('orgId'), async (req, res) => { + if (reservationOpsDisabled(res)) return; + try { + const { Event } = getModels(req, 'Event'); + const { orgId, eventId } = req.params; + const { action = 'acknowledged', note = '', assignedTo = null } = req.body || {}; + const event = await Event.findOne(buildScopedEventQuery(orgId, eventId)); + if (!event) return res.status(404).json({ success: false, message: 'Event not found' }); + const service = new ResourceReservationService(req); + service.applyExceptionState(event, { action, note, assignedTo, actorId: req.user?.userId || null }); + await event.save(); + return res.status(200).json({ success: true, data: event.reservation }); + } catch (error) { + return res.status(500).json({ success: false, message: error.message }); + } +}); + +router.get('/:orgId/reservations/metrics', verifyToken, requireEventManagement('orgId'), async (req, res) => { + if (reservationOpsDisabled(res)) return; + try { + const { orgId } = req.params; + const { startDate, endDate, format = 'json' } = req.query; + const service = new ReservationMetricsService(req); + const metrics = await service.getMetrics({ orgId, startDate, endDate }); + if (format === 'csv') { + res.setHeader('Content-Type', 'text/csv'); + res.setHeader('Content-Disposition', 'attachment; filename="reservation-metrics.csv"'); + return res.send(ReservationMetricsService.toCsv(metrics)); + } + return res.status(200).json({ success: true, data: metrics }); + } catch (error) { + return res.status(500).json({ success: false, message: error.message }); + } +}); + +router.post('/:orgId/reservations/:eventId/sync/dry-run', verifyToken, requireEventManagement('orgId'), async (req, res) => { + if (reservationOpsDisabled(res)) return; + try { + const { Event } = getModels(req, 'Event'); + const { orgId, eventId } = req.params; + const event = await Event.findOne(buildScopedEventQuery(orgId, eventId)); + if (!event) return res.status(404).json({ success: false, message: 'Event not found' }); + const syncService = new ExternalRoomSyncService(req); + const result = await syncService.dryRunSyncReservation(event, req.body || {}); + event.reservation = { + ...(event.reservation || {}), + sync: { + ...((event.reservation && event.reservation.sync) || {}), + sourceOfTruth: 'external', + externalProvider: result.provider || '', + externalResourceId: result.externalResourceId || '', + lastDryRunAt: result.checkedAt || new Date(), + lastDryRunStatus: result.status || '' + } + }; + await event.save(); + return res.status(200).json({ success: true, data: result }); + } catch (error) { + return res.status(500).json({ success: false, message: error.message }); + } +}); + module.exports = router; diff --git a/backend/routes/orgManagementRoutes.js b/backend/routes/orgManagementRoutes.js index 91bc0c2e..3a345dfe 100644 --- a/backend/routes/orgManagementRoutes.js +++ b/backend/routes/orgManagementRoutes.js @@ -1,5 +1,6 @@ const express = require('express'); const multer = require('multer'); +const mongoose = require('mongoose'); const path = require('path'); const getModels = require('../services/getModelService'); const { verifyToken, authorizeRoles } = require('../middlewares/verifyToken'); @@ -7,9 +8,36 @@ const { requireAdmin } = require('../middlewares/requireAdmin'); const { uploadImageToS3, upload } = require('../services/imageUploadService'); const { emitToOrgApprovalRoom } = require('../socket'); const { clean, isProfane } = require('../services/profanityFilterService'); +const { + getEffectivePolicyFromConfig, + assertLifecycleTransition +} = require('../services/atlasPolicyService'); +const { recordMemberJoined, recordMemberRemoved } = require('../services/orgMembershipService'); +const budgetService = require('../services/budgetService'); +const adminTenantSummaryService = require('../services/adminTenantSummaryService'); +const adminTenantEventsService = require('../services/adminTenantEventsService'); +const adminTenantEventOperatorService = require('../services/adminTenantEventOperatorService'); +const rootOperatorUsersService = require('../services/rootOperatorUsersService'); const router = express.Router(); +/** Org schema has no timestamps; derive a stable created time from _id when createdAt is missing. */ +function resolveOrgCreatedAt(plainOrg) { + const raw = plainOrg.createdAt || plainOrg.created_at; + if (raw != null && raw !== '') { + const d = raw instanceof Date ? raw : new Date(raw); + if (!Number.isNaN(d.getTime())) return d; + } + if (plainOrg._id != null && mongoose.Types.ObjectId.isValid(plainOrg._id)) { + try { + return new mongoose.Types.ObjectId(String(plainOrg._id)).getTimestamp(); + } catch (e) { + return undefined; + } + } + return undefined; +} + const handleMulterError = (err, req, res, next) => { if (err instanceof multer.MulterError) { if (err.code === 'LIMIT_FILE_SIZE') { @@ -318,6 +346,7 @@ router.get('/config', verifyToken, async (req, res) => { } else { data.messaging = { ...MESSAGING_SCHEMA_DEFAULTS, ...data.messaging }; } + data.atlasPolicy = getEffectivePolicyFromConfig(config); console.log(`GET: /org-management/config`); res.status(200).json({ @@ -334,6 +363,41 @@ router.get('/config', verifyToken, async (req, res) => { } }); +router.get('/onboarding-config', verifyToken, async (req, res) => { + const { OrgManagementConfig } = getModels(req, 'OrgManagementConfig'); + try { + let config = await OrgManagementConfig.findOne(); + if (!config) { + config = new OrgManagementConfig(); + await config.save(); + } + const defaults = { + enabled: false, + welcomeTitle: 'Welcome to your community', + welcomeSubtitle: + 'A quick setup helps community managers and campus admins better support your interests.', + collectName: true, + collectInterests: true, + enforceMinInterests: true, + enforceMaxInterests: true, + minInterests: 1, + maxInterests: 6, + customSteps: [], + }; + const onboarding = { ...defaults, ...(config.userOnboarding || {}) }; + return res.status(200).json({ + success: true, + data: onboarding, + }); + } catch (error) { + console.error('GET /org-management/onboarding-config failed:', error); + return res.status(500).json({ + success: false, + message: error.message || 'Failed to load onboarding config', + }); + } +}); + // Update management configuration router.put('/config', verifyToken, requireAdmin, async (req, res) => { const { OrgManagementConfig } = getModels(req, 'OrgManagementConfig'); @@ -404,8 +468,566 @@ router.put('/config', verifyToken, requireAdmin, async (req, res) => { } }); +// ==================== ADMIN TENANT (read-only summary & events) ==================== + +router.get( + '/admin-tenant-summary', + verifyToken, + authorizeRoles('admin', 'developer', 'beta'), + async (req, res) => { + try { + const data = await adminTenantSummaryService.getAdminTenantSummary(req); + res.status(200).json({ success: true, data }); + } catch (error) { + console.error('GET /org-management/admin-tenant-summary failed:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to load admin tenant summary', + }); + } + } +); + +router.get( + '/admin-tenant-events', + verifyToken, + authorizeRoles('admin', 'developer', 'beta'), + async (req, res) => { + try { + const page = parseInt(String(req.query.page || '1'), 10); + const limit = parseInt(String(req.query.limit || '20'), 10); + const q = typeof req.query.q === 'string' ? req.query.q : ''; + const includePast = ['1', 'true', 'yes'].includes(String(req.query.includePast || '').toLowerCase()); + const data = await adminTenantEventsService.listAdminTenantUpcomingEvents(req, { + page, + limit, + q, + includePast, + }); + res.status(200).json({ success: true, data }); + } catch (error) { + console.error('GET /org-management/admin-tenant-events failed:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to load admin tenant events', + }); + } + } +); + +router.get( + '/admin-tenant-events/:eventId/dashboard', + verifyToken, + authorizeRoles('admin', 'developer', 'beta'), + async (req, res) => { + try { + const { eventId } = req.params; + const data = await adminTenantEventOperatorService.getAdminTenantEventDashboard(req, eventId); + if (!data) { + return res.status(404).json({ success: false, message: 'Event not found' }); + } + res.status(200).json({ success: true, data }); + } catch (error) { + console.error('GET /org-management/admin-tenant-events/:eventId/dashboard failed:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to load event dashboard', + }); + } + } +); + +router.get( + '/admin-tenant-events/:eventId/registration-responses', + verifyToken, + authorizeRoles('admin', 'developer', 'beta'), + async (req, res) => { + try { + const { eventId } = req.params; + const data = await adminTenantEventOperatorService.getAdminTenantEventRegistrationResponses(req, eventId); + if (!data) { + return res.status(404).json({ success: false, message: 'Event not found' }); + } + res.status(200).json({ success: true, data }); + } catch (error) { + console.error( + 'GET /org-management/admin-tenant-events/:eventId/registration-responses failed:', + error + ); + res.status(500).json({ + success: false, + message: error.message || 'Failed to load registration responses', + }); + } + } +); + +router.get( + '/admin-tenant-events/:eventId/rsvp-growth', + verifyToken, + authorizeRoles('admin', 'developer', 'beta'), + async (req, res) => { + try { + const { eventId } = req.params; + const data = await adminTenantEventOperatorService.getAdminTenantEventRsvpGrowth(req, eventId, { + timezone: req.query.timezone, + }); + if (!data) { + return res.status(404).json({ success: false, message: 'Event not found' }); + } + res.status(200).json({ success: true, data }); + } catch (error) { + console.error('GET /org-management/admin-tenant-events/:eventId/rsvp-growth failed:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to load RSVP growth', + }); + } + } +); + +// ==================== ROOT / COMMUNITY DASHBOARD: PEOPLE & ACCESS ==================== + +router.get( + '/root-operator-user-stats', + verifyToken, + authorizeRoles('admin', 'developer', 'beta'), + async (req, res) => { + try { + const data = await rootOperatorUsersService.getRootOperatorUserStats(req); + res.status(200).json({ success: true, data }); + } catch (error) { + console.error('GET /org-management/root-operator-user-stats failed:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to load user statistics', + }); + } + } +); + +router.get( + '/root-operator-users', + verifyToken, + authorizeRoles('admin', 'developer', 'beta'), + async (req, res) => { + try { + const q = typeof req.query.q === 'string' ? req.query.q : ''; + const limit = parseInt(String(req.query.limit || '25'), 10); + const role = typeof req.query.role === 'string' ? req.query.role : ''; + const data = await rootOperatorUsersService.searchRootOperatorUsers(req, { q, limit, role }); + res.status(200).json({ success: true, data }); + } catch (error) { + console.error('GET /org-management/root-operator-users failed:', error); + res.status(500).json({ + success: false, + message: error.message || 'Failed to search users', + }); + } + } +); + +router.post( + '/root-operator-users/:userId/role', + verifyToken, + authorizeRoles('admin', 'developer', 'beta'), + async (req, res) => { + try { + const { userId } = req.params; + const { role, assign } = req.body || {}; + const data = await rootOperatorUsersService.setRootOperatorUserRole(req, { + userId, + role, + assign: assign === true || assign === 'true', + }); + res.status(200).json({ success: true, data }); + } catch (error) { + const status = error.statusCode || 500; + console.error('POST /org-management/root-operator-users/:userId/role failed:', error); + res.status(status).json({ + success: false, + message: error.message || 'Failed to update role', + }); + } + } +); + +router.patch( + '/root-operator-users/:userId/access', + verifyToken, + authorizeRoles('admin', 'developer', 'beta'), + async (req, res) => { + try { + const { userId } = req.params; + const { accessSuspended } = req.body || {}; + const data = await rootOperatorUsersService.setRootOperatorAccessSuspended(req, { + userId, + accessSuspended: accessSuspended === true || accessSuspended === 'true', + }); + res.status(200).json({ success: true, data }); + } catch (error) { + const status = error.statusCode || 500; + console.error('PATCH /org-management/root-operator-users/:userId/access failed:', error); + res.status(status).json({ + success: false, + message: error.message || 'Failed to update access', + }); + } + } +); + +router.get( + '/user-onboarding-responses-summary', + verifyToken, + authorizeRoles('admin', 'developer', 'beta'), + async (req, res) => { + try { + const { User, OrgManagementConfig } = getModels(req, 'User', 'OrgManagementConfig'); + const config = await OrgManagementConfig.findOne().lean(); + const onboardingConfig = config?.userOnboarding || {}; + const customSteps = Array.isArray(onboardingConfig.customSteps) ? onboardingConfig.customSteps : []; + + const users = await User.find({}) + .select('tags onboardingResponses') + .lean(); + + const summary = { + totalUsers: users.length, + interests: { + totalTaggedUsers: 0, + optionCounts: {}, + }, + customSteps: customSteps.map((step) => ({ + id: step.id, + label: step.label, + type: step.type, + required: !!step.required, + responseCount: 0, + optionCounts: {}, + textSamples: [], + })), + }; + + const stepMap = new Map(summary.customSteps.map((s) => [String(s.id), s])); + + users.forEach((user) => { + const tags = Array.isArray(user.tags) ? user.tags : []; + if (tags.length > 0) summary.interests.totalTaggedUsers += 1; + tags.forEach((tag) => { + const key = String(tag || '').trim(); + if (!key) return; + summary.interests.optionCounts[key] = (summary.interests.optionCounts[key] || 0) + 1; + }); + + const responses = user.onboardingResponses && typeof user.onboardingResponses === 'object' + ? user.onboardingResponses + : {}; + + customSteps.forEach((step) => { + const s = stepMap.get(String(step.id)); + if (!s) return; + const value = responses[step.id]; + if (Array.isArray(value)) { + const cleaned = value.map((v) => String(v || '').trim()).filter(Boolean); + if (cleaned.length > 0) s.responseCount += 1; + cleaned.forEach((v) => { + s.optionCounts[v] = (s.optionCounts[v] || 0) + 1; + }); + return; + } + const normalized = String(value || '').trim(); + if (!normalized) return; + s.responseCount += 1; + if (step.type === 'single-select') { + s.optionCounts[normalized] = (s.optionCounts[normalized] || 0) + 1; + } else if (s.textSamples.length < 8) { + s.textSamples.push(normalized); + } + }); + }); + + return res.status(200).json({ + success: true, + data: summary, + }); + } catch (error) { + console.error('GET /org-management/user-onboarding-responses-summary failed:', error); + return res.status(500).json({ + success: false, + message: error.message || 'Failed to load onboarding responses summary', + }); + } + } +); + // ==================== ORGANIZATION ANALYTICS ==================== +function parseOrgOverviewTimeRange(rawRange) { + const now = new Date(); + const range = ['7d', '30d', '90d', '1y'].includes(rawRange) ? rawRange : '30d'; + let startDate; + switch (range) { + case '7d': + startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + break; + case '90d': + startDate = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000); + break; + case '1y': + startDate = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000); + break; + case '30d': + default: + startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + break; + } + return { range, startDate, endDate: now }; +} + +// New org overview endpoint backed by analytics_events (platform analytics pipeline) +router.get('/analytics/platform-overview', verifyToken, requireAdmin, async (req, res) => { + const { Org, OrgMember, OrgVerification, AnalyticsEvent, Event } = getModels( + req, + 'Org', + 'OrgMember', + 'OrgVerification', + 'AnalyticsEvent', + 'Event' + ); + const { timeRange = '30d' } = req.query; + + try { + const { range, startDate, endDate } = parseOrgOverviewTimeRange(timeRange); + const match = { + ts: { $gte: startDate, $lte: endDate }, + event: { $in: ['event_view', 'event_registration'] } + }; + + const [totalOrgs, verifiedOrgs, newOrgs, memberStats, verificationStats, totalsByType, trendRows, sourceRows] = await Promise.all([ + Org.countDocuments(), + Org.countDocuments({ verified: true }), + Org.countDocuments({ createdAt: { $gte: startDate } }), + OrgMember.aggregate([ + { + $group: { + _id: null, + totalMembers: { $sum: 1 } + } + } + ]), + OrgVerification.aggregate([ + { + $group: { + _id: '$status', + count: { $sum: 1 } + } + } + ]), + AnalyticsEvent.aggregate([ + { $match: match }, + { $group: { _id: '$event', count: { $sum: 1 } } } + ]), + AnalyticsEvent.aggregate([ + { $match: match }, + { + $group: { + _id: { + date: { $dateToString: { format: '%Y-%m-%d', date: '$ts' } }, + event: '$event' + }, + count: { $sum: 1 } + } + }, + { $sort: { '_id.date': 1 } } + ]), + AnalyticsEvent.aggregate([ + { $match: { ...match, event: 'event_view' } }, + { + $project: { + source: { + $ifNull: [ + '$properties.source', + { + $switch: { + branches: [ + { case: { $regexMatch: { input: { $ifNull: ['$context.referrer', ''] }, regex: 'org/|club-dashboard' } }, then: 'org_page' }, + { case: { $regexMatch: { input: { $ifNull: ['$context.referrer', ''] }, regex: 'events-dashboard' } }, then: 'explore' } + ], + default: 'direct' + } + } + ] + } + } + }, + { + $group: { + _id: '$source', + count: { $sum: 1 } + } + } + ]) + ]); + + const totalsMap = totalsByType.reduce((acc, row) => { + acc[row._id] = row; + return acc; + }, {}); + const totalViews = totalsMap.event_view?.count || 0; + const totalRegistrations = totalsMap.event_registration?.count || 0; + const registrationRate = totalViews > 0 ? Number(((totalRegistrations / totalViews) * 100).toFixed(1)) : 0; + const [uniqueViewActorCount, uniqueRegistrantActorCount] = await Promise.all([ + AnalyticsEvent.aggregate([ + { $match: { ...match, event: 'event_view' } }, + { + $project: { + actor: { + $ifNull: [ + { $concat: ['u:', { $toString: '$user_id' }] }, + { $concat: ['a:', { $ifNull: ['$anonymous_id', ''] }] } + ] + } + } + }, + { $match: { actor: { $ne: 'a:' } } }, + { $group: { _id: '$actor' } }, + { $count: 'count' } + ]), + AnalyticsEvent.aggregate([ + { $match: { ...match, event: 'event_registration' } }, + { + $project: { + actor: { + $ifNull: [ + { $concat: ['u:', { $toString: '$user_id' }] }, + { $concat: ['a:', { $ifNull: ['$anonymous_id', ''] }] } + ] + } + } + }, + { $match: { actor: { $ne: 'a:' } } }, + { $group: { _id: '$actor' } }, + { $count: 'count' } + ]) + ]); + const uniqueViewers = uniqueViewActorCount[0]?.count || 0; + const uniqueRegistrants = uniqueRegistrantActorCount[0]?.count || 0; + + const trendMap = {}; + for (const row of trendRows) { + const date = row._id?.date; + const event = row._id?.event; + if (!date) continue; + if (!trendMap[date]) trendMap[date] = { date, views: 0, registrations: 0 }; + if (event === 'event_view') trendMap[date].views = row.count; + if (event === 'event_registration') trendMap[date].registrations = row.count; + } + const trends = Object.values(trendMap).sort((a, b) => (a.date < b.date ? -1 : 1)); + + const viewSources = { direct: 0, explore: 0, org_page: 0, email: 0, qr: 0 }; + for (const row of sourceRows) { + const key = String(row._id || '').toLowerCase(); + if (Object.prototype.hasOwnProperty.call(viewSources, key)) { + viewSources[key] = row.count; + } else { + viewSources.direct += row.count; + } + } + + const topEventCounts = await AnalyticsEvent.aggregate([ + { + $match: { + ts: { $gte: startDate, $lte: endDate }, + event: { $in: ['event_view', 'event_registration'] }, + 'properties.event_id': { $exists: true, $ne: null } + } + }, + { + $group: { + _id: { + eventId: { $toString: '$properties.event_id' }, + event: '$event' + }, + count: { $sum: 1 } + } + } + ]); + + const topEventIds = [...new Set(topEventCounts.map((r) => r?._id?.eventId).filter(Boolean))]; + const topEventObjIds = topEventIds + .filter((id) => mongoose.Types.ObjectId.isValid(id)) + .map((id) => new mongoose.Types.ObjectId(id)); + const eventRows = topEventObjIds.length + ? await Event.find({ + _id: { $in: topEventObjIds }, + hostingType: 'Org' + }).select('_id hostingId').lean() + : []; + const eventToOrgMap = new Map(eventRows.map((row) => [String(row._id), String(row.hostingId)])); + + const orgMetricMap = new Map(); + for (const row of topEventCounts) { + const eventId = row?._id?.eventId ? String(row._id.eventId) : ''; + const orgId = eventToOrgMap.get(eventId) || ''; + if (!orgId) continue; + if (!orgMetricMap.has(orgId)) orgMetricMap.set(orgId, { orgId, views: 0, registrations: 0 }); + const current = orgMetricMap.get(orgId); + if (row?._id?.event === 'event_view') current.views = row.count; + if (row?._id?.event === 'event_registration') current.registrations = row.count; + } + + const rankedOrgMetrics = [...orgMetricMap.values()] + .map((row) => ({ ...row, score: row.views + row.registrations * 4 })) + .sort((a, b) => b.score - a.score) + .slice(0, 6); + + const orgDocs = rankedOrgMetrics.length + ? await Org.find({ _id: { $in: rankedOrgMetrics.map((r) => r.orgId) } }).select('org_name').lean() + : []; + const orgNameMap = Object.fromEntries(orgDocs.map((o) => [String(o._id), o.org_name || 'Unknown org'])); + + const topOrganizations = rankedOrgMetrics.map((row) => ({ + orgId: row.orgId, + orgName: orgNameMap[row.orgId] || 'Unknown org', + views: row.views, + registrations: row.registrations, + score: row.score + })); + + res.status(200).json({ + success: true, + data: { + timeRange: range, + window: { startDate, endDate }, + overview: { + totalOrgs, + verifiedOrgs, + newOrgs, + totalMembers: memberStats[0]?.totalMembers || 0, + pendingVerificationRequests: verificationStats.find((s) => s._id === 'pending')?.count || 0 + }, + engagement: { + totalViews, + totalRegistrations, + registrationRate, + uniqueViewers, + uniqueRegistrants + }, + trends, + viewSources, + topOrganizations + } + }); + } catch (error) { + console.error('Error fetching org platform overview analytics:', error); + res.status(500).json({ + success: false, + message: 'Error fetching org platform overview analytics', + error: error.message + }); + } +}); + // Get organization analytics router.get('/analytics', verifyToken, requireAdmin, async (req, res) => { const { Org, OrgMember, Event, OrgVerification } = getModels(req, 'Org', 'OrgMember', 'Event', 'OrgVerification'); @@ -545,17 +1167,55 @@ router.get('/analytics', verifyToken, requireAdmin, async (req, res) => { // ==================== ORGANIZATION MANAGEMENT ==================== +// Draft governance versions awaiting platform admin approval (must be before /organizations/:orgId) +router.get('/governance/pending-drafts', verifyToken, requireAdmin, async (req, res) => { + const { Org } = getModels(req, 'Org'); + try { + const orgs = await Org.find({ + 'governanceDocuments.versions.status': 'draft' + }) + .select('org_name org_profile_image governanceDocuments') + .lean(); + + const rows = []; + for (const org of orgs) { + for (const slot of org.governanceDocuments || []) { + for (const ver of slot.versions || []) { + if (ver.status === 'draft') { + rows.push({ + orgId: org._id.toString(), + orgName: org.org_name, + orgProfileImage: org.org_profile_image || null, + docKey: slot.key, + version: ver.version, + uploadedAt: ver.uploadedAt, + originalFilename: ver.originalFilename || null, + storageUrl: ver.storageUrl || null + }); + } + } + } + } + rows.sort((a, b) => new Date(b.uploadedAt || 0) - new Date(a.uploadedAt || 0)); + res.status(200).json({ success: true, data: rows }); + } catch (error) { + console.error('Error listing pending governance drafts:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + // Get all organizations with management data router.get('/organizations', verifyToken, requireAdmin, async (req, res) => { const { Org, OrgMember, Event } = getModels(req, 'Org', 'OrgMember', 'Event'); - const { - search, - status, - verified, - page = 1, - limit = 20, - sortBy = 'createdAt', - sortOrder = 'desc' + const { + search, + status, + lifecycleStatus, + verified, + page: pageRaw = 1, + limit: limitRaw = 20, + sortBy = 'createdAt', + sortOrder = 'desc' } = req.query; try { @@ -569,9 +1229,19 @@ router.get('/organizations', verifyToken, requireAdmin, async (req, res) => { } if (status) filter.status = status; - if (verified !== '') filter.verified = verified === 'true'; + if (lifecycleStatus) filter.lifecycleStatus = lifecycleStatus; + // Only apply when explicitly requested; missing/undefined verified must not imply false + if (verified === 'true' || verified === 'false') { + filter.verified = verified === 'true'; + } + + const pageNum = Math.max(1, parseInt(String(pageRaw), 10) || 1); + const limitParsed = parseInt(String(limitRaw), 10); + const limitNum = Number.isFinite(limitParsed) && limitParsed > 0 + ? Math.min(limitParsed, 100) + : 20; - const skip = (parseInt(page) - 1) * parseInt(limit); + const skip = (pageNum - 1) * limitNum; const sort = { [sortBy]: sortOrder === 'desc' ? -1 : 1 }; console.log(filter); @@ -579,7 +1249,7 @@ router.get('/organizations', verifyToken, requireAdmin, async (req, res) => { const orgs = await Org.find(filter) .sort(sort) .skip(skip) - .limit(parseInt(limit)); + .limit(limitNum); // Get additional data for each org const orgsWithData = await Promise.all(orgs.map(async (org) => { @@ -590,8 +1260,14 @@ router.get('/organizations', verifyToken, requireAdmin, async (req, res) => { createdAt: { $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) } }); + const plain = org.toObject(); + const createdAt = resolveOrgCreatedAt(plain); + const createdAtOut = + createdAt != null && !Number.isNaN(createdAt.getTime()) ? createdAt : null; + return { - ...org.toObject(), + ...plain, + createdAt: createdAtOut, memberCount, recentEventCount: eventCount }; @@ -600,14 +1276,16 @@ router.get('/organizations', verifyToken, requireAdmin, async (req, res) => { const total = await Org.countDocuments(filter); console.log(`GET: /org-management/organizations - Retrieved ${orgsWithData.length} organizations`); + const totalPages = Math.max(1, Math.ceil(total / limitNum)); + res.status(200).json({ success: true, data: orgsWithData, pagination: { - page: parseInt(page), - limit: parseInt(limit), + page: pageNum, + limit: limitNum, total, - totalPages: Math.ceil(total / limit) + totalPages } }); } catch (error) { @@ -675,13 +1353,443 @@ router.get('/organizations/export', verifyToken, requireAdmin, async (req, res) } }); +/** Platform admin: org-hosted events time window for snapshot / list. */ +function parseAdminOrgEventTime(query) { + const range = ['7d', '30d', '90d', '1y'].includes(query.range) ? query.range : '30d'; + const windowMode = query.window === 'upcoming' ? 'upcoming' : 'past'; + const now = new Date(); + const dayMs = 24 * 60 * 60 * 1000; + const rangeMs = + range === '7d' ? 7 * dayMs + : range === '90d' ? 90 * dayMs + : range === '1y' ? 365 * dayMs + : 30 * dayMs; + let startDate; + let endDate; + if (windowMode === 'past') { + endDate = now; + startDate = new Date(now.getTime() - rangeMs); + } else { + startDate = now; + endDate = new Date(now.getTime() + rangeMs); + } + return { startDate, endDate, range, windowMode }; +} + +function adminOrgEventEngagementScore(ev, an) { + const attendeeCount = ev.registrationCount ?? (Array.isArray(ev.attendees) ? ev.attendees.length : 0); + const ur = an?.uniqueRegistrations ?? 0; + const r = an?.registrations ?? 0; + const uv = an?.uniqueViews ?? 0; + const v = an?.views ?? 0; + return ur * 5 + r * 2 + attendeeCount * 3 + uv * 0.08 + v * 0.02 + (ev.expectedAttendance || 0) * 0.05; +} + +function deriveEventViewSource(raw) { + const source = raw?.properties?.source; + if (['org_page', 'explore', 'direct', 'email'].includes(source)) return source; + const referrer = String(raw?.context?.referrer || ''); + if (referrer.includes('org/') || referrer.includes('club-dashboard')) return 'org_page'; + if (referrer.includes('events-dashboard')) return 'explore'; + return 'direct'; +} + +function buildAnalyticsEventIdMatch(eventIds) { + const strIds = [...new Set((eventIds || []).map((id) => String(id)).filter(Boolean))]; + const objIds = strIds + .filter((id) => mongoose.Types.ObjectId.isValid(id)) + .map((id) => new mongoose.Types.ObjectId(id)); + return { + strIds, + match: { + $or: [ + { 'properties.event_id': { $in: strIds } }, + { 'properties.event_id': { $in: objIds } } + ] + } + }; +} + +function finalizeAnalyticsBuckets(bucketMap) { + const out = new Map(); + for (const [eventId, b] of bucketMap.entries()) { + out.set(eventId, { + views: b.views, + uniqueViews: b.uniqueViewActors.size, + anonymousViews: b.anonymousViews, + uniqueAnonymousViews: b.uniqueAnonymousActors.size, + registrations: b.registrations, + uniqueRegistrations: b.uniqueRegistrationActors.size, + sources: b.sources + }); + } + return out; +} + +async function buildAnalyticsByEventId(AnalyticsEvent, eventIds) { + const { strIds, match } = buildAnalyticsEventIdMatch(eventIds); + if (!strIds.length) return new Map(); + + const rows = await AnalyticsEvent.find({ + event: { $in: ['event_view', 'event_registration'] }, + ...match + }) + .select('event user_id anonymous_id session_id properties context ts') + .lean(); + + const targetIds = new Set(strIds); + const buckets = new Map(); + + for (const row of rows) { + const eventId = row?.properties?.event_id != null ? String(row.properties.event_id) : ''; + if (!targetIds.has(eventId)) continue; + if (!buckets.has(eventId)) { + buckets.set(eventId, { + views: 0, + registrations: 0, + anonymousViews: 0, + uniqueViewActors: new Set(), + uniqueAnonymousActors: new Set(), + uniqueRegistrationActors: new Set(), + sources: { org_page: 0, explore: 0, direct: 0, email: 0 } + }); + } + const b = buckets.get(eventId); + const userKey = row.user_id ? `u:${String(row.user_id)}` : ''; + const anonKey = row.anonymous_id ? `a:${String(row.anonymous_id)}` : ''; + const sessionKey = row.session_id ? `s:${String(row.session_id)}` : ''; + const actor = userKey || anonKey || sessionKey || `e:${String(row._id)}`; + + if (row.event === 'event_view') { + b.views += 1; + b.uniqueViewActors.add(actor); + if (!userKey) { + b.anonymousViews += 1; + b.uniqueAnonymousActors.add(actor); + } + const source = deriveEventViewSource(row); + if (Object.prototype.hasOwnProperty.call(b.sources, source)) b.sources[source] += 1; + } + + if (row.event === 'event_registration') { + b.registrations += 1; + b.uniqueRegistrationActors.add(actor); + } + } + + return finalizeAnalyticsBuckets(buckets); +} + +// Admin: ranked "best" events in a time window (by engagement heuristics) +router.get('/organizations/:orgId/events/snapshot', verifyToken, requireAdmin, async (req, res) => { + const { Event, AnalyticsEvent } = getModels(req, 'Event', 'AnalyticsEvent'); + const { orgId } = req.params; + const topN = Math.min(Math.max(parseInt(String(req.query.top || '5'), 10) || 5, 1), 25); + const { startDate, endDate, range, windowMode } = parseAdminOrgEventTime(req.query); + + try { + const filter = { + hostingId: orgId, + hostingType: 'Org', + isDeleted: false, + start_time: { $gte: startDate, $lte: endDate } + }; + + const events = await Event.find(filter) + .select('name type start_time end_time location status registrationCount attendees expectedAttendance image') + .lean(); + + const ids = events.map((e) => e._id); + const analyticsById = await buildAnalyticsByEventId(AnalyticsEvent, ids); + + const enriched = events.map((ev) => { + const an = analyticsById.get(String(ev._id)); + const attendeeCount = ev.registrationCount ?? (Array.isArray(ev.attendees) ? ev.attendees.length : 0); + const score = adminOrgEventEngagementScore(ev, an); + return { + _id: ev._id, + name: ev.name, + type: ev.type, + start_time: ev.start_time, + end_time: ev.end_time, + location: ev.location, + status: ev.status, + image: ev.image, + expectedAttendance: ev.expectedAttendance, + registrationCount: attendeeCount, + analytics: { + views: an?.views ?? 0, + uniqueViews: an?.uniqueViews ?? 0, + registrations: an?.registrations ?? 0, + uniqueRegistrations: an?.uniqueRegistrations ?? 0 + }, + score + }; + }); + enriched.sort((a, b) => b.score - a.score); + const topEvents = enriched.slice(0, topN); + + res.status(200).json({ + success: true, + data: { + range, + window: windowMode, + startDate, + endDate, + totalInRange: enriched.length, + topEvents + } + }); + } catch (error) { + console.error('Error building org event snapshot (admin):', error); + res.status(500).json({ + success: false, + message: 'Error loading event snapshot', + error: error.message + }); + } +}); + +// Admin: paginated events in window (optional sort by engagement score) +router.get('/organizations/:orgId/events', verifyToken, requireAdmin, async (req, res) => { + const { Event, AnalyticsEvent } = getModels(req, 'Event', 'AnalyticsEvent'); + const { orgId } = req.params; + const pageNum = Math.max(1, parseInt(String(req.query.page || '1'), 10) || 1); + const limitNum = Math.min(Math.max(parseInt(String(req.query.limit || '15'), 10) || 15, 1), 50); + const sort = req.query.sort === 'start_time' ? 'start_time' : 'engagement'; + const { startDate, endDate, range, windowMode } = parseAdminOrgEventTime(req.query); + + try { + const filter = { + hostingId: orgId, + hostingType: 'Org', + isDeleted: false, + start_time: { $gte: startDate, $lte: endDate } + }; + + const events = await Event.find(filter) + .select('name type start_time end_time location status registrationCount attendees expectedAttendance image') + .lean(); + + const ids = events.map((e) => e._id); + const analyticsById = await buildAnalyticsByEventId(AnalyticsEvent, ids); + + const enriched = events.map((ev) => { + const an = analyticsById.get(String(ev._id)); + const attendeeCount = ev.registrationCount ?? (Array.isArray(ev.attendees) ? ev.attendees.length : 0); + const score = adminOrgEventEngagementScore(ev, an); + return { + _id: ev._id, + name: ev.name, + type: ev.type, + start_time: ev.start_time, + end_time: ev.end_time, + location: ev.location, + status: ev.status, + image: ev.image, + expectedAttendance: ev.expectedAttendance, + registrationCount: attendeeCount, + analytics: { + views: an?.views ?? 0, + uniqueViews: an?.uniqueViews ?? 0, + registrations: an?.registrations ?? 0, + uniqueRegistrations: an?.uniqueRegistrations ?? 0 + }, + score + }; + }); + + if (sort === 'start_time') { + enriched.sort((a, b) => new Date(a.start_time) - new Date(b.start_time)); + } else { + enriched.sort((a, b) => b.score - a.score); + } + + const total = enriched.length; + const skip = (pageNum - 1) * limitNum; + const pageRows = enriched.slice(skip, skip + limitNum); + + res.status(200).json({ + success: true, + data: { + range, + window: windowMode, + startDate, + endDate, + events: pageRows, + pagination: { + page: pageNum, + limit: limitNum, + total, + totalPages: Math.max(1, Math.ceil(total / limitNum)) + } + } + }); + } catch (error) { + console.error('Error listing org events (admin):', error); + res.status(500).json({ + success: false, + message: 'Error loading events', + error: error.message + }); + } +}); + +// Admin: deep engagement + ops snapshot for one org-hosted event +router.get('/organizations/:orgId/events/:eventId/engagement', verifyToken, requireAdmin, async (req, res) => { + const { Event, AnalyticsEvent, EventAgenda, EventJob, VolunteerSignup, EventEquipment } = getModels( + req, + 'Event', + 'AnalyticsEvent', + 'EventAgenda', + 'EventJob', + 'VolunteerSignup', + 'EventEquipment' + ); + const { orgId, eventId } = req.params; + + try { + const event = await Event.findOne({ + _id: eventId, + hostingId: orgId, + hostingType: 'Org', + isDeleted: false + }) + .populate('hostingId', 'org_name org_profile_image') + .populate('collaboratorOrgs.orgId', 'org_name org_profile_image') + .lean(); + + if (!event) { + return res.status(404).json({ success: false, message: 'Event not found' }); + } + + const analyticsByEvent = await buildAnalyticsByEventId(AnalyticsEvent, [eventId]); + const analytics = analyticsByEvent.get(String(eventId)) || null; + const agenda = await EventAgenda.findOne({ eventId }).lean(); + const roles = await EventJob.find({ eventId }).lean(); + const totalVolunteers = roles.reduce((sum, role) => sum + (role.assignments?.length || 0), 0); + const confirmedVolunteers = roles.reduce( + (sum, role) => sum + (role.assignments?.filter((a) => a.status === 'confirmed')?.length || 0), + 0 + ); + const signups = await VolunteerSignup.find({ eventId }).populate('memberId', 'name email').lean(); + const equipment = await EventEquipment.findOne({ eventId }).lean(); + + const registrationCount = event.registrationCount ?? (event.attendees?.length ?? 0); + const checkedInCount = signups.filter((s) => s.checkedIn).length; + + let eventCheckIn = null; + if (event.checkInEnabled && event.attendees && Array.isArray(event.attendees)) { + const totalCheckedIn = event.attendees.filter((a) => a.checkedIn).length; + const totalRegistrations = event.registrationCount ?? event.attendees.length; + eventCheckIn = { + totalCheckedIn, + totalRegistrations, + checkInRate: totalRegistrations > 0 + ? ((totalCheckedIn / totalRegistrations) * 100).toFixed(1) + : '0' + }; + } + + const now = new Date(); + let operationalStatus = 'upcoming'; + if (event.start_time <= now && event.end_time >= now) { + operationalStatus = 'active'; + } else if (event.end_time < now) { + operationalStatus = 'completed'; + } + + const { match } = buildAnalyticsEventIdMatch([eventId]); + const historyRows = await AnalyticsEvent.find({ + event: { $in: ['event_view', 'event_registration'] }, + ...match + }) + .select('event ts user_id anonymous_id session_id properties context') + .sort({ ts: -1 }) + .limit(400) + .lean(); + + const viewHistory = historyRows + .filter((r) => r.event === 'event_view') + .slice(0, 40) + .map((r) => ({ + timestamp: r.ts, + isAnonymous: !r.user_id, + source: deriveEventViewSource(r) + })); + + const registrationHistory = historyRows + .filter((r) => r.event === 'event_registration') + .slice(0, 40) + .map((r) => ({ + timestamp: r.ts, + isAnonymous: !r.user_id + })); + + res.status(200).json({ + success: true, + data: { + event, + analytics: analytics + ? { + views: analytics.views, + uniqueViews: analytics.uniqueViews, + anonymousViews: analytics.anonymousViews, + uniqueAnonymousViews: analytics.uniqueAnonymousViews, + registrations: analytics.registrations, + uniqueRegistrations: analytics.uniqueRegistrations, + sources: analytics.sources || { org_page: 0, explore: 0, direct: 0, email: 0 }, + viewHistorySample: viewHistory, + registrationHistorySample: registrationHistory + } + : { + views: 0, + uniqueViews: 0, + anonymousViews: 0, + uniqueAnonymousViews: 0, + registrations: 0, + uniqueRegistrations: 0, + sources: { org_page: 0, explore: 0, direct: 0, email: 0 }, + viewHistorySample: [], + registrationHistorySample: [] + }, + agenda: agenda || { items: [] }, + roles: { + total: roles.length, + assignments: totalVolunteers, + confirmed: confirmedVolunteers, + signups: signups.length + }, + equipment: equipment || { items: [] }, + stats: { + registrationCount, + volunteers: { + total: totalVolunteers, + confirmed: confirmedVolunteers, + checkedIn: checkedInCount + }, + operationalStatus, + checkIn: eventCheckIn + } + } + }); + } catch (error) { + console.error('Error loading org event engagement (admin):', error); + res.status(500).json({ + success: false, + message: 'Error loading event engagement', + error: error.message + }); + } +}); + // Get single organization by ID (admin only) router.get('/organizations/:orgId', verifyToken, requireAdmin, async (req, res) => { const { Org, OrgMember, Event } = getModels(req, 'Org', 'OrgMember', 'Event'); const { orgId } = req.params; try { - const org = await Org.findById(orgId).populate('owner', 'username name email'); + const org = await Org.findById(orgId).populate('owner', 'username name email picture'); if (!org) { return res.status(404).json({ success: false, @@ -690,16 +1798,30 @@ router.get('/organizations/:orgId', verifyToken, requireAdmin, async (req, res) } const memberCount = await OrgMember.countDocuments({ org_id: orgId }); - const eventCount = await Event.countDocuments({ + const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); + const recentEventCount = await Event.countDocuments({ hostingId: orgId, hostingType: 'Org', - createdAt: { $gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) } + createdAt: { $gte: thirtyDaysAgo } }); + const totalEventCount = await Event.countDocuments({ + hostingId: orgId, + hostingType: 'Org' + }); + + const plain = org.toObject(); + const createdResolved = resolveOrgCreatedAt(plain); + const createdAtOut = + createdResolved != null && !Number.isNaN(createdResolved.getTime()) + ? createdResolved + : null; const orgWithData = { - ...org.toObject(), + ...plain, + createdAt: createdAtOut, memberCount, - recentEventCount: eventCount + recentEventCount, + totalEventCount }; console.log(`GET: /org-management/organizations/${orgId} - Retrieved org`); @@ -957,9 +2079,11 @@ router.post('/organizations/:orgId/members', verifyToken, requireAdmin, async (r org_id: orgId, user_id: userId, role, + status: 'active', assignedBy: req.user.userId }); await member.save(); + await recordMemberJoined(member, req.user.userId, 'admin_added'); if (!user.clubAssociations || !user.clubAssociations.some(c => c.toString() === orgId)) { if (!user.clubAssociations) user.clubAssociations = []; @@ -1017,6 +2141,12 @@ router.delete('/organizations/:orgId/members/:userId', verifyToken, requireAdmin }); } + await recordMemberRemoved(req, { + org_id: orgId, + user_id: userId, + actorUserId: req.user.userId, + reason: 'admin_removed' + }); await OrgMember.deleteOne({ _id: member._id }); const user = await User.findById(userId); @@ -1092,7 +2222,10 @@ router.put('/organizations/:orgId/members/:userId/role', verifyToken, requireAdm }); } - await member.changeRole(role, req.user.userId, 'Role changed by admin'); + await member.changeRole(role, req.user.userId, 'Role changed by admin', { + termStart: req.body.roleTermStart ? new Date(req.body.roleTermStart) : undefined, + termEnd: req.body.roleTermEnd ? new Date(req.body.roleTermEnd) : undefined + }); if (!user.clubAssociations || !user.clubAssociations.some(c => c.toString() === orgId)) { if (!user.clubAssociations) user.clubAssociations = []; @@ -1165,11 +2298,11 @@ router.put('/organizations/:orgId/approve', verifyToken, requireAdmin, async (re } }); -// Update organization status +// Update organization (verification flags, admin notes, optional orgTypeKey — not lifecycle; use PATCH lifecycle) router.put('/organizations/:orgId', verifyToken, requireAdmin, async (req, res) => { const { Org } = getModels(req, 'Org'); const { orgId } = req.params; - const { verified, status, notes } = req.body; + const { verified, notes, orgTypeKey } = req.body; const adminId = req.user.userId; try { @@ -1187,8 +2320,8 @@ router.put('/organizations/:orgId', verifyToken, requireAdmin, async (req, res) org.verifiedBy = verified ? adminId : null; } - if (status) org.status = status; - if (notes) org.adminNotes = notes; + if (notes !== undefined) org.adminNotes = notes; + if (orgTypeKey !== undefined) org.orgTypeKey = orgTypeKey; await org.save(); @@ -1208,53 +2341,210 @@ router.put('/organizations/:orgId', verifyToken, requireAdmin, async (req, res) } }); -// ==================== MIGRATIONS ==================== +// PATCH lifecycle (platform admin) +router.patch('/organizations/:orgId/lifecycle', verifyToken, requireAdmin, async (req, res) => { + const { Org, OrgManagementConfig } = getModels(req, 'Org', 'OrgManagementConfig'); + const { orgId } = req.params; + const { lifecycleStatus: nextStatus } = req.body; + const adminId = req.user.userId; -/** - * Add _id to org positions that don't have it (for role rename detection). - * Run once per tenant. Protected: admin or root only. - */ -router.post('/migrate/org-positions-ids', verifyToken, requireAdmin, async (req, res) => { - const mongoose = require('mongoose'); + try { + if (!nextStatus) { + return res.status(400).json({ success: false, message: 'lifecycleStatus is required' }); + } + const org = await Org.findById(orgId); + if (!org) { + return res.status(404).json({ success: false, message: 'Organization not found' }); + } + const config = await OrgManagementConfig.findOne(); + const policy = getEffectivePolicyFromConfig(config); + assertLifecycleTransition(policy, org, nextStatus, { isPlatformAdmin: true, isOfficer: false }); + org.lifecycleStatus = nextStatus; + org.lifecycleChangedAt = new Date(); + org.lifecycleChangedBy = adminId; + await org.save(); + res.status(200).json({ + success: true, + message: 'Lifecycle updated', + data: org + }); + } catch (error) { + const code = error.statusCode || 500; + console.error('Error updating lifecycle:', error); + res.status(code).json({ + success: false, + message: error.message || 'Error updating lifecycle' + }); + } +}); + +// Approve a governance document version (platform admin) +router.put('/organizations/:orgId/governance/:docKey/versions/:version/approve', verifyToken, requireAdmin, async (req, res) => { const { Org } = getModels(req, 'Org'); + const { orgId, docKey, version } = req.params; + const adminId = req.user.userId; + const verNum = parseInt(version, 10); try { - const orgs = await Org.find({}).lean(); - let updated = 0; + if (Number.isNaN(verNum)) { + return res.status(400).json({ success: false, message: 'Invalid version' }); + } + const org = await Org.findById(orgId); + if (!org) { + return res.status(404).json({ success: false, message: 'Organization not found' }); + } + const slot = org.getGovernanceSlot(docKey); + if (!slot || !slot.versions?.length) { + return res.status(404).json({ success: false, message: 'Governance document not found' }); + } + const v = slot.versions.find((x) => x.version === verNum); + if (!v) { + return res.status(404).json({ success: false, message: 'Version not found' }); + } + for (const x of slot.versions) { + if (x.status === 'approved') { + x.status = 'superseded'; + } + } + v.status = 'approved'; + v.approvedBy = adminId; + v.approvedAt = new Date(); + org.markModified('governanceDocuments'); + await org.save(); + res.status(200).json({ + success: true, + message: 'Version approved', + data: org.governanceDocuments + }); + } catch (error) { + console.error('Governance approve error:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); - for (const org of orgs) { - const positions = org.positions || []; - let changed = false; +// ==================== FINANCE (CMS Phase 2) ==================== - for (let i = 0; i < positions.length; i++) { - if (!positions[i]._id) { - positions[i]._id = new mongoose.Types.ObjectId(); - changed = true; - } - } +router.get('/finance/config', verifyToken, requireAdmin, async (req, res) => { + try { + const data = await budgetService.ensureFinanceConfig(req); + res.status(200).json({ success: true, data }); + } catch (error) { + console.error('GET finance config:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); - if (changed) { - await Org.updateOne( - { _id: org._id }, - { $set: { positions } } - ); - updated++; - } - } +router.put('/finance/config', verifyToken, requireAdmin, async (req, res) => { + try { + const data = await budgetService.updateFinanceConfig(req, req.body || {}); + res.status(200).json({ success: true, data }); + } catch (error) { + console.error('PUT finance config:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); - console.log(`POST: /org-management/migrate/org-positions-ids - Updated ${updated} org(s)`); +router.get('/finance/budgets', verifyToken, requireAdmin, async (req, res) => { + try { + const result = await budgetService.listBudgetsAdmin(req, req.query); res.status(200).json({ success: true, - message: `Migration completed. Updated ${updated} organization(s) with _id on role positions.`, - data: { orgsUpdated: updated } + data: result.data, + pagination: { + page: result.page, + limit: result.limit, + total: result.total, + totalPages: Math.ceil(result.total / result.limit) + } }); } catch (error) { - console.error('Migration org-positions-ids failed:', error); - res.status(500).json({ - success: false, - message: error.message || 'Migration failed' - }); + console.error('GET finance budgets:', error); + res.status(500).json({ success: false, message: error.message }); } }); +router.get( + '/organizations/:orgId/budgets/:budgetId', + verifyToken, + requireAdmin, + async (req, res) => { + try { + const { orgId, budgetId } = req.params; + const data = await budgetService.getBudgetById(req, orgId, budgetId); + if (!data) { + return res.status(404).json({ success: false, message: 'Budget not found' }); + } + res.status(200).json({ success: true, data }); + } catch (error) { + console.error('GET admin budget:', error); + res.status(500).json({ success: false, message: error.message }); + } + } +); + +router.put( + '/organizations/:orgId/budgets/:budgetId/stages/:stageKey/approve', + verifyToken, + requireAdmin, + async (req, res) => { + try { + const { orgId, budgetId, stageKey } = req.params; + const data = await budgetService.approveStagePlatform(req, orgId, budgetId, req.user.userId, stageKey); + res.status(200).json({ success: true, data }); + } catch (error) { + const code = error.statusCode || 500; + console.error('Platform budget approve:', error); + res.status(code).json({ success: false, message: error.message }); + } + } +); + +router.put( + '/organizations/:orgId/budgets/:budgetId/stages/:stageKey/reject', + verifyToken, + requireAdmin, + async (req, res) => { + try { + const { orgId, budgetId, stageKey } = req.params; + const data = await budgetService.rejectBudget( + req, + orgId, + budgetId, + req.user.userId, + { ...(req.body || {}), stageKey }, + { platformOnly: true } + ); + res.status(200).json({ success: true, data }); + } catch (error) { + const code = error.statusCode || 500; + console.error('Platform budget reject:', error); + res.status(code).json({ success: false, message: error.message }); + } + } +); + +router.put( + '/organizations/:orgId/budgets/:budgetId/stages/:stageKey/request-revision', + verifyToken, + requireAdmin, + async (req, res) => { + try { + const { orgId, budgetId, stageKey } = req.params; + const data = await budgetService.requestRevision( + req, + orgId, + budgetId, + req.user.userId, + { ...(req.body || {}), stageKey }, + { platformOnly: true } + ); + res.status(200).json({ success: true, data }); + } catch (error) { + const code = error.statusCode || 500; + console.error('Platform budget request-revision:', error); + res.status(code).json({ success: false, message: error.message }); + } + } +); + module.exports = router; diff --git a/backend/routes/orgRoleRoutes.js b/backend/routes/orgRoleRoutes.js index bccbb12b..7f33fcd3 100644 --- a/backend/routes/orgRoleRoutes.js +++ b/backend/routes/orgRoleRoutes.js @@ -1,12 +1,21 @@ const express = require('express'); const getModels = require('../services/getModelService'); const { verifyToken } = require('../middlewares/verifyToken'); -const { - requireOrgOwner, - requireRoleManagement, +const { + requireOrgOwner, + requireRoleManagement, requireMemberManagement, - requireOrgPermission + requireOrgPermission, + requireAnyOrgPermission } = require('../middlewares/orgPermissions'); +const { governanceUpload } = require('../services/imageUploadService'); +const { + getEffectivePolicyFromConfig, + assertLifecycleTransition, + governanceRequirementsForOrg, + labelForGovernanceKey +} = require('../services/atlasPolicyService'); +const { recordMemberJoined, recordMemberRemoved } = require('../services/orgMembershipService'); const router = express.Router(); @@ -421,20 +430,22 @@ router.post('/:orgId/members/:userId/role', verifyToken, async (req, res) => { let member = await OrgMember.findOne({ org_id: orgId, user_id: userId }); if (!member) { - // Create new member record member = new OrgMember({ org_id: orgId, user_id: userId, role: role, - assignedBy: req.user.userId + assignedBy: req.user.userId, + status: 'active' }); + await member.save(); + await recordMemberJoined(member, req.user.userId, reason || 'role_assigned'); } else { - // Update existing member's role - await member.changeRole(role, req.user.userId, reason); + await member.changeRole(role, req.user.userId, reason, { + termStart: req.body.roleTermStart ? new Date(req.body.roleTermStart) : undefined, + termEnd: req.body.roleTermEnd ? new Date(req.body.roleTermEnd) : undefined + }); } - await member.save(); - // Check auto-approve for org (Atlas: when member count reaches threshold) const { checkAndAutoApproveOrg } = require('../services/orgApprovalService'); await checkAndAutoApproveOrg(req, orgId); @@ -520,6 +531,12 @@ router.delete('/:orgId/members/:userId', verifyToken, requireMemberManagement(), // Soft delete by setting status to inactive // member.status = 'inactive'; // await member.save(); + await recordMemberRemoved(req, { + org_id: orgId, + user_id: userId, + actorUserId: req.user.userId, + reason: 'removed_by_org_manager' + }); await OrgMember.deleteOne({ _id: member._id }); res.status(200).json({ @@ -638,6 +655,7 @@ router.post('/:orgId/applications/:applicationId/approve', verifyToken, requireM }); await newMember.save(); + await recordMemberJoined(newMember, userId, 'application_approved'); // Check auto-approve for org (Atlas: when member count reaches threshold) const { checkAndAutoApproveOrg } = require('../services/orgApprovalService'); @@ -758,4 +776,150 @@ router.get('/:orgId/applications', verifyToken, requireMemberManagement(), async } }); +// --- Atlas CMS Phase 1: lifecycle, governance, membership history --- + +router.patch('/:orgId/lifecycle', verifyToken, requireAnyOrgPermission(['manage_roles', 'manage_settings']), async (req, res) => { + const { Org, OrgManagementConfig } = getModels(req, 'Org', 'OrgManagementConfig'); + const { orgId } = req.params; + const { lifecycleStatus: nextStatus } = req.body; + + try { + if (!nextStatus) { + return res.status(400).json({ success: false, message: 'lifecycleStatus is required' }); + } + const org = await Org.findById(orgId); + if (!org) { + return res.status(404).json({ success: false, message: 'Organization not found' }); + } + const config = await OrgManagementConfig.findOne(); + const policy = getEffectivePolicyFromConfig(config); + assertLifecycleTransition(policy, org, nextStatus, { isPlatformAdmin: false, isOfficer: true }); + org.lifecycleStatus = nextStatus; + org.lifecycleChangedAt = new Date(); + org.lifecycleChangedBy = req.user.userId; + await org.save(); + res.status(200).json({ success: true, message: 'Lifecycle updated', data: org }); + } catch (error) { + const code = error.statusCode || 500; + console.error('PATCH lifecycle error:', error); + res.status(code).json({ + success: false, + message: error.message || 'Error updating lifecycle' + }); + } +}); + +router.get('/:orgId/governance', verifyToken, requireOrgPermission('view_events'), async (req, res) => { + const { Org } = getModels(req, 'Org'); + const { orgId } = req.params; + try { + const org = await Org.findById(orgId); + if (!org) { + return res.status(404).json({ success: false, message: 'Organization not found' }); + } + res.status(200).json({ + success: true, + documents: org.governanceDocuments || [] + }); + } catch (error) { + console.error('GET governance error:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + +router.get('/:orgId/governance/requirements', verifyToken, requireOrgPermission('view_events'), async (req, res) => { + const { Org, OrgManagementConfig } = getModels(req, 'Org', 'OrgManagementConfig'); + const { orgId } = req.params; + try { + const org = await Org.findById(orgId); + if (!org) { + return res.status(404).json({ success: false, message: 'Organization not found' }); + } + const config = await OrgManagementConfig.findOne(); + const policy = getEffectivePolicyFromConfig(config); + const requiredKeys = governanceRequirementsForOrg(policy, org); + const labels = {}; + for (const k of requiredKeys) { + labels[k] = labelForGovernanceKey(policy, k); + } + res.status(200).json({ + success: true, + requiredKeys, + labels, + terminology: policy.terminology || {} + }); + } catch (error) { + console.error('GET governance requirements error:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + +router.post('/:orgId/governance/:docKey/upload', verifyToken, requireOrgPermission('manage_governance'), governanceUpload.single('file'), async (req, res) => { + const { Org } = getModels(req, 'Org'); + const { uploadDocumentToS3 } = require('../services/imageUploadService'); + const { orgId, docKey } = req.params; + + try { + if (!req.file) { + return res.status(400).json({ success: false, message: 'PDF file is required (field name: file)' }); + } + const org = await Org.findById(orgId); + if (!org) { + return res.status(404).json({ success: false, message: 'Organization not found' }); + } + const safeKey = String(docKey).replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64); + if (!safeKey) { + return res.status(400).json({ success: false, message: 'Invalid document key' }); + } + const fileName = `governance-${orgId}-${safeKey}`; + const storageUrl = await uploadDocumentToS3(req.file, 'orgs/governance', fileName); + const effectiveFrom = req.body.effectiveFrom ? new Date(req.body.effectiveFrom) : undefined; + org.addGovernanceVersion(safeKey, { + storageUrl, + originalFilename: req.file.originalname, + mimeType: req.file.mimetype, + uploadedBy: req.user.userId, + uploadedAt: new Date(), + effectiveFrom, + status: 'draft' + }); + await org.save(); + res.status(201).json({ + success: true, + message: 'Governance document version uploaded', + documents: org.governanceDocuments + }); + } catch (error) { + console.error('POST governance upload error:', error); + res.status(500).json({ + success: false, + message: error.message || 'Upload failed' + }); + } +}); + +router.get('/:orgId/members/:userId/history', verifyToken, requireMemberManagement(), async (req, res) => { + const { OrgMember, OrgMembershipAudit } = getModels(req, 'OrgMember', 'OrgMembershipAudit'); + const { orgId, userId } = req.params; + + try { + const member = await OrgMember.findOne({ org_id: orgId, user_id: userId }).lean(); + const audits = await OrgMembershipAudit.find({ org_id: orgId, user_id: userId }) + .sort({ at: -1 }) + .limit(100) + .lean(); + res.status(200).json({ + success: true, + membershipHistory: member?.membershipHistory || [], + roleHistory: member?.roleHistory || [], + roleTermStart: member?.roleTermStart, + roleTermEnd: member?.roleTermEnd, + removedAudits: audits + }); + } catch (error) { + console.error('GET member history error:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/backend/routes/orgRoutes.js b/backend/routes/orgRoutes.js index 757b99c9..bbf66b3c 100644 --- a/backend/routes/orgRoutes.js +++ b/backend/routes/orgRoutes.js @@ -16,6 +16,8 @@ const path = require('path'); const { uploadImageToS3, upload } = require('../services/imageUploadService'); const { requireMemberManagement, requireOrgOwner } = require('../middlewares/orgPermissions'); const NotificationService = require('../services/notificationService'); +const { getEffectivePolicyFromConfig } = require('../services/atlasPolicyService'); +const { recordMemberJoined } = require('../services/orgMembershipService'); // Error handling middleware for multer const handleMulterError = (err, req, res, next) => { @@ -206,6 +208,7 @@ router.post("/create-org", verifyToken, upload.fields([ const config = await OrgManagementConfig.findOne(); const approvalMode = config?.orgApproval?.mode || 'none'; const requiresApproval = ['manual', 'auto', 'both'].includes(approvalMode); + const atlasPolicy = getEffectivePolicyFromConfig(config || {}); // Prepare default roles (only owner and member) const defaultRoles = [ @@ -287,6 +290,8 @@ router.post("/create-org", verifyToken, upload.fields([ owner: userId, approvalStatus: requiresApproval ? 'pending' : 'approved', requireApprovalForJoin: requireApprovalForJoin === 'true' || requireApprovalForJoin === true, + lifecycleStatus: atlasPolicy.lifecycle?.defaultStatus || 'active', + orgTypeKey: atlasPolicy.defaultOrgTypeKey || 'default' }); // Handle profile image upload if file is present @@ -320,6 +325,7 @@ router.post("/create-org", verifyToken, upload.fields([ }); await newMember.save(); + await recordMemberJoined(newMember, userId, 'org_created'); user.clubAssociations.push(newOrg._id); await user.save(); @@ -1084,9 +1090,15 @@ router.post('/send-email', async (req,res) => { router.get('/get-orgs', verifyTokenOptional, async (req, res) => { try { const { exhaustive, includePending } = req.query; - const { Org, OrgMember, OrgFollower, Event } = getModels(req, 'Org', 'OrgMember', 'OrgFollower', 'Event'); + const { Org, OrgMember, OrgFollower, Event, OrgManagementConfig } = getModels(req, 'Org', 'OrgMember', 'OrgFollower', 'Event', 'OrgManagementConfig'); const userId = req.user?.userId; + const mgmtConfig = await OrgManagementConfig.findOne(); + const atlasPolicy = getEffectivePolicyFromConfig(mgmtConfig || {}); + const hideNonActive = + atlasPolicy.directory?.hideNonActiveFromPublicList && + includePending !== 'true'; + // Atlas: Default filter - only approved orgs for discoverability. includePending + auth = add user's orgs. const baseApprovalFilter = { $or: [ @@ -1114,10 +1126,25 @@ router.get('/get-orgs', verifyTokenOptional, async (req, res) => { // Exclude unlisted orgs from the public Organizations list const unlistedFilter = { unlisted: { $ne: true } }; + const matchParts = [ + approvalFilter, + unlistedFilter, + { isDeleted: { $ne: true } } + ]; + if (hideNonActive) { + const hideStatuses = atlasPolicy.directory?.nonActiveStatuses || ['inactive']; + matchParts.push({ + $or: [ + { lifecycleStatus: { $exists: false } }, + { lifecycleStatus: { $nin: hideStatuses } } + ] + }); + } + //if exhaustive, return org stats like memberCount, followerCount, eventCount, using aggregate using efficeint query if (exhaustive) { const orgs = await Org.aggregate([ - { $match: { ...approvalFilter, ...unlistedFilter, isDeleted: { $ne: true } } }, + { $match: { $and: matchParts } }, // Lookup members and count them (only active members) { $lookup: { @@ -1181,6 +1208,8 @@ router.get('/get-orgs', verifyTokenOptional, async (req, res) => { verified: 1, verificationType: 1, verificationStatus: 1, + lifecycleStatus: 1, + orgTypeKey: 1, memberCount: { $size: '$members' }, followerCount: { $size: '$followers' }, eventCount: { $size: '$events' } @@ -1193,7 +1222,7 @@ router.get('/get-orgs', verifyTokenOptional, async (req, res) => { orgs: orgs }); } - const orgs = await Org.find({ ...approvalFilter, ...unlistedFilter }); + const orgs = await Org.find({ $and: matchParts }); res.status(200).json({ success: true, orgs diff --git a/backend/routes/roomRoutes.js b/backend/routes/roomRoutes.js index b969cd04..e3b441a6 100644 --- a/backend/routes/roomRoutes.js +++ b/backend/routes/roomRoutes.js @@ -1,4 +1,5 @@ const express = require('express'); +const mongoose = require('mongoose'); const { verifyToken, authorizeRoles } = require('../middlewares/verifyToken'); const { requireAdmin } = require('../middlewares/requireAdmin'); const getModels = require('../services/getModelService'); @@ -30,6 +31,7 @@ router.get('/rooms', verifyToken, requireAdmin, async (req, res) => { const [rooms, total] = await Promise.all([ Classroom.find(filter) + .populate('building', 'name') .sort({ name: 1 }) .skip(skip) .limit(parseInt(limit)), @@ -56,7 +58,7 @@ router.get('/rooms', verifyToken, requireAdmin, async (req, res) => { router.get('/rooms/:id', verifyToken, requireAdmin, async (req, res) => { const { Classroom } = getModels(req, 'Classroom'); try { - const room = await Classroom.findById(req.params.id); + const room = await Classroom.findById(req.params.id).populate('building', 'name'); if (!room) return res.status(404).json({ success: false, message: 'Room not found' }); res.status(200).json({ success: true, room }); } catch (error) { @@ -68,13 +70,17 @@ router.get('/rooms/:id', verifyToken, requireAdmin, async (req, res) => { // Create room router.post('/rooms', verifyToken, requireAdmin, async (req, res) => { const { Classroom } = getModels(req, 'Classroom'); - const { name, image = '/classrooms/default.png', attributes = [], mainSearch = true } = req.body; + const { name, image = '/classrooms/default.png', attributes = [], mainSearch = true, building } = req.body; try { if (!name) return res.status(400).json({ success: false, message: 'Name is required' }); const existing = await Classroom.findOne({ name }); if (existing) return res.status(400).json({ success: false, message: 'Room name already exists' }); - const room = new Classroom({ name, image, attributes, mainSearch }); + const payload = { name, image, attributes, mainSearch }; + if (building && mongoose.Types.ObjectId.isValid(String(building))) { + payload.building = new mongoose.Types.ObjectId(String(building)); + } + const room = new Classroom(payload); await room.save(); res.status(201).json({ success: true, room }); } catch (error) { @@ -86,13 +92,25 @@ router.post('/rooms', verifyToken, requireAdmin, async (req, res) => { // Update room router.put('/rooms/:id', verifyToken, requireAdmin, async (req, res) => { const { Classroom } = getModels(req, 'Classroom'); - const updates = req.body; + const updates = { ...req.body }; + if (Object.prototype.hasOwnProperty.call(updates, 'building')) { + if (updates.building === '' || updates.building == null) { + updates.building = null; + } else if (mongoose.Types.ObjectId.isValid(String(updates.building))) { + updates.building = new mongoose.Types.ObjectId(String(updates.building)); + } else { + delete updates.building; + } + } try { if (updates.name) { const exists = await Classroom.findOne({ name: updates.name, _id: { $ne: req.params.id } }); if (exists) return res.status(400).json({ success: false, message: 'Room name already exists' }); } - const room = await Classroom.findByIdAndUpdate(req.params.id, updates, { new: true }); + const room = await Classroom.findByIdAndUpdate(req.params.id, updates, { new: true }).populate( + 'building', + 'name' + ); if (!room) return res.status(404).json({ success: false, message: 'Room not found' }); res.status(200).json({ success: true, room }); } catch (error) { @@ -119,7 +137,7 @@ const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 5 * router.post('/rooms/:id/image', verifyToken, requireAdmin, upload.single('image'), async (req, res) => { const { Classroom } = getModels(req, 'Classroom'); try { - const room = await Classroom.findById(req.params.id); + const room = await Classroom.findById(req.params.id).populate('building', 'name'); if (!room) return res.status(404).json({ success: false, message: 'Room not found' }); if (!req.file) return res.status(400).json({ success: false, message: 'No file uploaded' }); @@ -135,6 +153,115 @@ router.post('/rooms/:id/image', verifyToken, requireAdmin, upload.single('image' } }); +const DEFAULT_BUILDING_TIME = { start: 0, end: 24 * 60 }; + +function normalizeBuildingTime(body) { + const t = body?.time && typeof body.time === 'object' ? body.time : body; + const start = Number.isFinite(Number(t?.start)) ? Number(t.start) : DEFAULT_BUILDING_TIME.start; + const end = Number.isFinite(Number(t?.end)) ? Number(t.end) : DEFAULT_BUILDING_TIME.end; + return { start, end }; +} + +// --- Buildings (campus structures linked from classrooms) --- + +router.get('/buildings', verifyToken, requireAdmin, async (req, res) => { + const { Building } = getModels(req, 'Building'); + const { search = '', page = 1, limit = 20 } = req.query; + try { + const filter = search + ? { name: { $regex: String(search).trim(), $options: 'i' } } + : {}; + const skip = (Math.max(1, parseInt(page, 10)) - 1) * Math.max(1, parseInt(limit, 10)); + const lim = Math.min(100, Math.max(1, parseInt(limit, 10))); + const [buildings, total] = await Promise.all([ + Building.find(filter).sort({ name: 1 }).skip(skip).limit(lim).lean(), + Building.countDocuments(filter), + ]); + res.status(200).json({ + success: true, + buildings, + pagination: { + total, + totalPages: Math.ceil(total / lim), + currentPage: parseInt(page, 10) || 1, + limit: lim, + }, + }); + } catch (error) { + console.error('GET /buildings failed', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + +router.post('/buildings', verifyToken, requireAdmin, async (req, res) => { + const { Building } = getModels(req, 'Building'); + try { + const name = String(req.body?.name || '').trim(); + if (!name) return res.status(400).json({ success: false, message: 'Name is required' }); + const image = String(req.body?.image || '').trim() || '/classrooms/default.png'; + const time = normalizeBuildingTime(req.body); + const dup = await Building.findOne({ name: new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i') }); + if (dup) return res.status(400).json({ success: false, message: 'A building with this name already exists' }); + const building = new Building({ name, image, time }); + await building.save(); + res.status(201).json({ success: true, building }); + } catch (error) { + console.error('POST /buildings failed', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + +router.put('/buildings/:id', verifyToken, requireAdmin, async (req, res) => { + const { Building } = getModels(req, 'Building'); + try { + if (!mongoose.Types.ObjectId.isValid(req.params.id)) { + return res.status(400).json({ success: false, message: 'Invalid building id' }); + } + const updates = {}; + if (req.body.name != null) { + const name = String(req.body.name).trim(); + if (!name) return res.status(400).json({ success: false, message: 'Name cannot be empty' }); + updates.name = name; + } + if (req.body.image != null) { + const image = String(req.body.image).trim(); + if (!image) return res.status(400).json({ success: false, message: 'Image is required' }); + updates.image = image; + } + if (req.body.time != null || req.body.start != null || req.body.end != null) { + updates.time = normalizeBuildingTime(req.body); + } + const building = await Building.findByIdAndUpdate(req.params.id, { $set: updates }, { new: true }).lean(); + if (!building) return res.status(404).json({ success: false, message: 'Building not found' }); + res.status(200).json({ success: true, building }); + } catch (error) { + console.error('PUT /buildings/:id failed', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + +router.delete('/buildings/:id', verifyToken, requireAdmin, async (req, res) => { + const { Building, Classroom } = getModels(req, 'Building', 'Classroom'); + try { + if (!mongoose.Types.ObjectId.isValid(req.params.id)) { + return res.status(400).json({ success: false, message: 'Invalid building id' }); + } + const inUse = await Classroom.countDocuments({ building: req.params.id }); + if (inUse > 0) { + return res.status(400).json({ + success: false, + message: `Cannot delete: ${inUse} classroom(s) still reference this building.`, + }); + } + const building = await Building.findByIdAndDelete(req.params.id); + if (!building) return res.status(404).json({ success: false, message: 'Building not found' }); + res.status(200).json({ success: true, message: 'Building deleted' }); + } catch (error) { + console.error('DELETE /buildings/:id failed', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + module.exports = router; diff --git a/backend/routes/searchRoutes.js b/backend/routes/searchRoutes.js index f5d236b8..e3ac5274 100644 --- a/backend/routes/searchRoutes.js +++ b/backend/routes/searchRoutes.js @@ -107,7 +107,7 @@ router.get('/search', verifyTokenOptional, async (req, res) => { sortedClassrooms.map(async (classroom) => { try { // Get full classroom data - const fullRoom = await Classroom.findById(classroom._id); + const fullRoom = await Classroom.findById(classroom._id).populate('building', 'name'); // Get schedule data const schedule = await Schedule.findOne({ classroom_id: classroom._id }); @@ -178,6 +178,7 @@ router.get('/search-rooms', verifyTokenOptional, async (req, res) => { // Execute search with pagination const [rooms, total] = await Promise.all([ Classroom.find(searchQuery) + .populate('building', 'name') .sort({ name: 1 }) .skip(skip) .limit(parseInt(limit)), @@ -473,6 +474,7 @@ router.get('/unified-search', verifyTokenOptional, async (req, res) => { // Rooms Classroom.find(roomQuery) + .populate('building', 'name') .limit(parseInt(limit)) .lean(), diff --git a/backend/routes/taskManagementRoutes.js b/backend/routes/taskManagementRoutes.js new file mode 100644 index 00000000..7e8de8ef --- /dev/null +++ b/backend/routes/taskManagementRoutes.js @@ -0,0 +1,714 @@ +const express = require('express'); +const router = express.Router(); +const { verifyToken } = require('../middlewares/verifyToken'); +const { requireEventManagement } = require('../middlewares/orgPermissions'); +const getModels = require('../services/getModelService'); +const { + asObjectId, + computeDueAtForTask, + computeEventReadiness, + getSuggestedTasksForEvent, + listTasks, + findOneTaskDto, + buildEventTaskAssigneeSummary, + recomputeDueDatesForEvent, + sortHubTasks, + normalizeTaskStatusForOrg, + getResolvedTaskBoardStatuses, + applyTaskColumnOrder +} = require('../services/taskService'); +const { + validateTaskBoardStatusesPayload, + getAllowedStatusKeys, + DEFAULT_TASK_BOARD_STATUSES +} = require('../services/taskBoardStatusUtils'); + +function toBoolean(value, defaultValue = false) { + if (value === undefined || value === null || value === '') return defaultValue; + if (typeof value === 'boolean') return value; + if (typeof value === 'number') return value !== 0; + const normalized = String(value).trim().toLowerCase(); + return ['1', 'true', 'yes', 'y', 'on'].includes(normalized); +} + +function applyTaskPatch(task, payload = {}, statusConfig) { + const cfg = statusConfig || DEFAULT_TASK_BOARD_STATUSES; + const scalarFields = [ + 'title', + 'description', + 'status', + 'priority', + 'source', + 'userConfirmed', + 'dueAt', + 'dueRule', + 'blockers', + 'integrationLinks', + 'tags', + 'metadata' + ]; + scalarFields.forEach((field) => { + if (payload[field] !== undefined) { + if ((field === 'title' || field === 'description') && typeof payload[field] === 'string') { + task[field] = payload[field].trim(); + return; + } + if (field === 'status') { + task[field] = normalizeTaskStatusForOrg(payload[field], cfg); + return; + } + task[field] = payload[field]; + } + }); + if (payload.ownerUserId !== undefined) { + task.ownerUserId = payload.ownerUserId ? asObjectId(payload.ownerUserId) : null; + } + if (payload.watcherUserIds !== undefined) { + if (Array.isArray(payload.watcherUserIds)) { + task.watcherUserIds = payload.watcherUserIds + .map((id) => asObjectId(id)) + .filter(Boolean); + } else { + task.watcherUserIds = []; + } + } + if (payload.eventId !== undefined) { + task.eventId = payload.eventId ? asObjectId(payload.eventId) : null; + } + if (payload.isCritical !== undefined) { + task.isCritical = toBoolean(payload.isCritical, false); + } + if (payload.boardRank !== undefined && payload.boardRank !== null) { + const n = Number(payload.boardRank); + if (Number.isFinite(n)) { + task.boardRank = n; + } + } +} + +// Org task board columns (Kanban / status workflow; max 10) +router.get('/:orgId/task-board-statuses', verifyToken, requireEventManagement('orgId'), async (req, res) => { + const { orgId } = req.params; + const models = getModels(req, 'Org'); + try { + const org = await models.Org.findById(asObjectId(orgId)).select('taskBoardStatuses').lean(); + return res.status(200).json({ + success: true, + data: { statuses: getResolvedTaskBoardStatuses(org) } + }); + } catch (error) { + console.error('Error loading task board statuses:', error); + return res.status(500).json({ success: false, message: 'Error loading task board statuses', error: error.message }); + } +}); + +router.put('/:orgId/task-board-statuses', verifyToken, requireEventManagement('orgId'), async (req, res) => { + const { orgId } = req.params; + const models = getModels(req, 'Org', 'Task'); + const body = req.body || {}; + + try { + const org = await models.Org.findById(asObjectId(orgId)); + if (!org) { + return res.status(404).json({ success: false, message: 'Organization not found' }); + } + + if (body.reset === true) { + org.taskBoardStatuses = undefined; + await org.save(); + return res.status(200).json({ + success: true, + data: { statuses: getResolvedTaskBoardStatuses(null) } + }); + } + + const parsed = validateTaskBoardStatusesPayload(body.statuses); + if (parsed.error) { + return res.status(400).json({ success: false, message: parsed.error }); + } + + const prevKeys = getAllowedStatusKeys(getResolvedTaskBoardStatuses(org)); + const nextKeys = new Set(parsed.value.map((s) => s.key)); + const removed = [...prevKeys].filter((k) => !nextKeys.has(k)); + if (removed.length) { + const inUse = await models.Task.countDocuments({ + orgId: asObjectId(orgId), + status: { $in: removed } + }); + if (inUse > 0) { + return res.status(400).json({ + success: false, + message: `Cannot remove column(s) still used by ${inUse} task(s). Move those tasks first.` + }); + } + } + + org.taskBoardStatuses = parsed.value; + await org.save(); + return res.status(200).json({ + success: true, + data: { statuses: getResolvedTaskBoardStatuses(org) } + }); + } catch (error) { + console.error('Error saving task board statuses:', error); + return res.status(500).json({ success: false, message: 'Error saving task board statuses', error: error.message }); + } +}); + +// Distinct task assignees per event (for event list / quick look avatars) +router.get('/:orgId/tasks/event-assignee-summary', verifyToken, requireEventManagement('orgId'), async (req, res) => { + const { orgId } = req.params; + const models = getModels(req, 'Task', 'User'); + + try { + const assigneesByEventId = await buildEventTaskAssigneeSummary(models, orgId); + return res.status(200).json({ + success: true, + data: { assigneesByEventId } + }); + } catch (error) { + console.error('Error building event task assignee summary:', error); + return res.status(500).json({ + success: false, + message: 'Error loading task assignees', + error: error.message + }); + } +}); + +async function loadOrgTaskBoardConfig(models, orgId) { + if (!models?.Org) return DEFAULT_TASK_BOARD_STATUSES; + const org = await models.Org.findById(asObjectId(orgId)).select('taskBoardStatuses').lean(); + return getResolvedTaskBoardStatuses(org); +} + +async function ensureOrgEventAccess(models, orgId, eventId) { + if (!eventId) return null; + const event = await models.Event.findOne({ + _id: asObjectId(eventId), + hostingType: 'Org', + isDeleted: false, + $or: [ + { hostingId: asObjectId(orgId) }, + { collaboratorOrgs: { $elemMatch: { orgId: asObjectId(orgId), status: 'active' } } } + ] + }).lean(); + return event || null; +} + +// Event-level task list +router.get('/:orgId/events/:eventId/tasks', verifyToken, requireEventManagement('orgId'), async (req, res) => { + const { orgId, eventId } = req.params; + const { status = 'all', ownerUserId, priority = 'all', search = '', sortBy = 'priority' } = req.query; + const models = getModels(req, 'Task', 'Event', 'Org'); + + try { + const event = await ensureOrgEventAccess(models, orgId, eventId); + if (!event) { + return res.status(404).json({ success: false, message: 'Event not found' }); + } + + const tasks = await listTasks(models, orgId, { + eventId, + status, + ownerUserId, + priority, + search, + sortBy + }); + + return res.status(200).json({ + success: true, + data: { + event: { + _id: event._id, + name: event.name, + start_time: event.start_time, + end_time: event.end_time + }, + tasks + } + }); + } catch (error) { + console.error('Error listing event tasks:', error); + return res.status(500).json({ success: false, message: 'Error listing event tasks', error: error.message }); + } +}); + +// Persist drag order within one status column (event tasks) +router.put('/:orgId/events/:eventId/tasks/column-order', verifyToken, requireEventManagement('orgId'), async (req, res) => { + const { orgId, eventId } = req.params; + const { taskIds } = req.body || {}; + const models = getModels(req, 'Task', 'Event'); + + try { + const event = await ensureOrgEventAccess(models, orgId, eventId); + if (!event) { + return res.status(404).json({ success: false, message: 'Event not found' }); + } + if (!Array.isArray(taskIds)) { + return res.status(400).json({ success: false, message: 'taskIds must be an array' }); + } + await applyTaskColumnOrder(models, orgId, taskIds, eventId); + return res.status(200).json({ success: true, message: 'Order updated' }); + } catch (error) { + console.error('Error updating event task column order:', error); + return res.status(500).json({ success: false, message: 'Error updating order', error: error.message }); + } +}); + +// Get single event task +router.get('/:orgId/events/:eventId/tasks/:taskId', verifyToken, requireEventManagement('orgId'), async (req, res) => { + const { orgId, eventId, taskId } = req.params; + const models = getModels(req, 'Task', 'Event', 'Org'); + + try { + const event = await ensureOrgEventAccess(models, orgId, eventId); + if (!event) { + return res.status(404).json({ success: false, message: 'Event not found' }); + } + const task = await findOneTaskDto(models, orgId, taskId, eventId); + if (!task) { + return res.status(404).json({ success: false, message: 'Task not found' }); + } + return res.status(200).json({ success: true, data: { task } }); + } catch (error) { + console.error('Error loading event task:', error); + return res.status(500).json({ success: false, message: 'Error loading task', error: error.message }); + } +}); + +// Create event task +router.post('/:orgId/events/:eventId/tasks', verifyToken, requireEventManagement('orgId'), async (req, res) => { + const { orgId, eventId } = req.params; + const payload = req.body || {}; + const models = getModels(req, 'Task', 'Event', 'Org'); + + try { + const event = await ensureOrgEventAccess(models, orgId, eventId); + if (!event) { + return res.status(404).json({ success: false, message: 'Event not found' }); + } + if (!payload.title || !String(payload.title).trim()) { + return res.status(400).json({ success: false, message: 'Task title is required' }); + } + + const statusConfig = await loadOrgTaskBoardConfig(models, orgId); + const task = new models.Task({ + orgId: asObjectId(orgId), + eventId: asObjectId(eventId), + title: String(payload.title).trim(), + description: payload.description || '', + status: normalizeTaskStatusForOrg(payload.status || 'todo', statusConfig), + priority: payload.priority || 'medium', + isCritical: toBoolean(payload.isCritical, false), + ownerUserId: payload.ownerUserId && asObjectId(payload.ownerUserId) ? asObjectId(payload.ownerUserId) : null, + source: payload.source || 'manual', + userConfirmed: payload.userConfirmed !== undefined ? toBoolean(payload.userConfirmed, false) : true, + dueRule: payload.dueRule || { anchorType: 'none' }, + blockers: Array.isArray(payload.blockers) ? payload.blockers : [], + integrationLinks: Array.isArray(payload.integrationLinks) ? payload.integrationLinks : [], + tags: Array.isArray(payload.tags) ? payload.tags : [] + }); + + task.dueAt = payload.dueAt ? new Date(payload.dueAt) : computeDueAtForTask(task, event, null); + await task.save(); + + return res.status(201).json({ + success: true, + message: 'Task created', + data: { task } + }); + } catch (error) { + console.error('Error creating event task:', error); + return res.status(500).json({ success: false, message: 'Error creating task', error: error.message }); + } +}); + +// Update event task +router.put('/:orgId/events/:eventId/tasks/:taskId', verifyToken, requireEventManagement('orgId'), async (req, res) => { + const { orgId, eventId, taskId } = req.params; + const payload = req.body || {}; + const models = getModels(req, 'Task', 'Event', 'Org'); + + try { + const event = await ensureOrgEventAccess(models, orgId, eventId); + if (!event) { + return res.status(404).json({ success: false, message: 'Event not found' }); + } + const task = await models.Task.findOne({ + _id: asObjectId(taskId), + orgId: asObjectId(orgId), + eventId: asObjectId(eventId) + }); + if (!task) { + return res.status(404).json({ success: false, message: 'Task not found' }); + } + + const statusConfig = await loadOrgTaskBoardConfig(models, orgId); + applyTaskPatch(task, payload, statusConfig); + if (payload.dueRule !== undefined && payload.dueAt === undefined) { + task.dueAt = computeDueAtForTask(task, event, null); + } + await task.save(); + + return res.status(200).json({ + success: true, + message: 'Task updated', + data: { task } + }); + } catch (error) { + console.error('Error updating event task:', error); + return res.status(500).json({ success: false, message: 'Error updating task', error: error.message }); + } +}); + +// Delete event task +router.delete('/:orgId/events/:eventId/tasks/:taskId', verifyToken, requireEventManagement('orgId'), async (req, res) => { + const { orgId, eventId, taskId } = req.params; + const models = getModels(req, 'Task', 'Event'); + + try { + const event = await ensureOrgEventAccess(models, orgId, eventId); + if (!event) { + return res.status(404).json({ success: false, message: 'Event not found' }); + } + const deleted = await models.Task.findOneAndDelete({ + _id: asObjectId(taskId), + orgId: asObjectId(orgId), + eventId: asObjectId(eventId) + }); + if (!deleted) { + return res.status(404).json({ success: false, message: 'Task not found' }); + } + return res.status(200).json({ success: true, message: 'Task deleted' }); + } catch (error) { + console.error('Error deleting event task:', error); + return res.status(500).json({ success: false, message: 'Error deleting task', error: error.message }); + } +}); + +// Recompute event task due dates from relative rules +router.post('/:orgId/events/:eventId/tasks/recompute-due-dates', verifyToken, requireEventManagement('orgId'), async (req, res) => { + const { orgId, eventId } = req.params; + const models = getModels(req, 'Task', 'Event', 'ApprovalInstance'); + + try { + const event = await ensureOrgEventAccess(models, orgId, eventId); + if (!event) { + return res.status(404).json({ success: false, message: 'Event not found' }); + } + const updatedCount = await recomputeDueDatesForEvent(models, orgId, eventId); + return res.status(200).json({ + success: true, + message: 'Due dates recomputed', + data: { updatedCount } + }); + } catch (error) { + console.error('Error recomputing due dates:', error); + return res.status(500).json({ success: false, message: 'Error recomputing due dates', error: error.message }); + } +}); + +// Event readiness snapshot +router.get('/:orgId/events/:eventId/readiness', verifyToken, requireEventManagement('orgId'), async (req, res) => { + const { orgId, eventId } = req.params; + const models = getModels(req, 'Task', 'Event', 'ApprovalInstance', 'EventEquipment', 'EventJob', 'Org'); + + try { + const event = await ensureOrgEventAccess(models, orgId, eventId); + if (!event) { + return res.status(404).json({ success: false, message: 'Event not found' }); + } + + const readiness = await computeEventReadiness(models, orgId, eventId); + return res.status(200).json({ + success: true, + data: readiness + }); + } catch (error) { + console.error('Error computing readiness:', error); + return res.status(500).json({ success: false, message: 'Error computing readiness', error: error.message }); + } +}); + +// Organization-level task hub +router.get('/:orgId/tasks/hub', verifyToken, requireEventManagement('orgId'), async (req, res) => { + const { + status = 'all', + ownerUserId, + priority = 'all', + eventId = 'all', + search = '', + onlyBlocked, + onlyOverdue, + sortBy = 'urgency' + } = req.query; + const { orgId } = req.params; + const models = getModels(req, 'Task', 'Event', 'Org'); + + try { + const tasks = await listTasks(models, orgId, { + eventId: eventId === 'all' ? undefined : eventId, + status, + ownerUserId, + priority, + search, + onlyBlocked: toBoolean(onlyBlocked, false), + onlyOverdue: toBoolean(onlyOverdue, false) + }); + + const sorted = sortHubTasks(tasks, sortBy); + const summary = { + total: sorted.length, + overdue: sorted.filter((task) => task.overdue).length, + blocked: sorted.filter((task) => task.effectiveStatus === 'blocked').length, + highPriority: sorted.filter((task) => ['high', 'critical'].includes(task.priority)).length + }; + + return res.status(200).json({ + success: true, + data: { + summary, + tasks: sorted + } + }); + } catch (error) { + console.error('Error loading task hub:', error); + return res.status(500).json({ success: false, message: 'Error loading task hub', error: error.message }); + } +}); + +// Get single hub task (org-scoped) +router.get('/:orgId/tasks/hub/:taskId', verifyToken, requireEventManagement('orgId'), async (req, res) => { + const { orgId, taskId } = req.params; + const models = getModels(req, 'Task', 'Org'); + + try { + const task = await findOneTaskDto(models, orgId, taskId, null); + if (!task) { + return res.status(404).json({ success: false, message: 'Task not found' }); + } + return res.status(200).json({ success: true, data: { task } }); + } catch (error) { + console.error('Error loading hub task:', error); + return res.status(500).json({ success: false, message: 'Error loading task', error: error.message }); + } +}); + +// Create organization-level operational task (non-event task) +router.post('/:orgId/tasks/hub', verifyToken, requireEventManagement('orgId'), async (req, res) => { + const { orgId } = req.params; + const payload = req.body || {}; + const models = getModels(req, 'Task', 'Org'); + + try { + if (!payload.title || !String(payload.title).trim()) { + return res.status(400).json({ success: false, message: 'Task title is required' }); + } + + const ownerObjectId = payload.ownerUserId && asObjectId(payload.ownerUserId); + const eventObjectId = payload.eventId && asObjectId(payload.eventId); + const statusConfig = await loadOrgTaskBoardConfig(models, orgId); + + const task = new models.Task({ + orgId: asObjectId(orgId), + eventId: eventObjectId || null, + title: String(payload.title).trim(), + description: payload.description || '', + status: normalizeTaskStatusForOrg(payload.status || 'todo', statusConfig), + priority: payload.priority || 'medium', + isCritical: toBoolean(payload.isCritical, false), + ownerUserId: ownerObjectId || null, + source: payload.source || 'manual', + userConfirmed: payload.userConfirmed !== undefined ? toBoolean(payload.userConfirmed, false) : true, + dueRule: payload.dueRule || { anchorType: 'none' }, + dueAt: payload.dueAt ? new Date(payload.dueAt) : null, + blockers: Array.isArray(payload.blockers) ? payload.blockers : [], + integrationLinks: Array.isArray(payload.integrationLinks) ? payload.integrationLinks : [], + tags: Array.isArray(payload.tags) ? payload.tags : [] + }); + + await task.save(); + return res.status(201).json({ + success: true, + message: 'Task created', + data: { task } + }); + } catch (error) { + console.error('Error creating hub task:', error); + return res.status(500).json({ success: false, message: 'Error creating task', error: error.message }); + } +}); + +// Persist drag order within one status column (org task hub — any eventId per task). +// MUST be registered before PUT /:orgId/tasks/hub/:taskId or "column-order" is parsed as taskId. +router.put('/:orgId/tasks/hub/column-order', verifyToken, requireEventManagement('orgId'), async (req, res) => { + const { orgId } = req.params; + const { taskIds } = req.body || {}; + const models = getModels(req, 'Task'); + + try { + if (!Array.isArray(taskIds)) { + return res.status(400).json({ success: false, message: 'taskIds must be an array' }); + } + await applyTaskColumnOrder(models, orgId, taskIds, undefined); + return res.status(200).json({ success: true, message: 'Order updated' }); + } catch (error) { + console.error('Error updating hub task column order:', error); + return res.status(500).json({ success: false, message: 'Error updating order', error: error.message }); + } +}); + +// Update organization-level task by id +router.put('/:orgId/tasks/hub/:taskId', verifyToken, requireEventManagement('orgId'), async (req, res) => { + const { orgId, taskId } = req.params; + const payload = req.body || {}; + const models = getModels(req, 'Task', 'Event', 'Org'); + + try { + const task = await models.Task.findOne({ + _id: asObjectId(taskId), + orgId: asObjectId(orgId) + }); + if (!task) { + return res.status(404).json({ success: false, message: 'Task not found' }); + } + + const statusConfig = await loadOrgTaskBoardConfig(models, orgId); + applyTaskPatch(task, payload, statusConfig); + + if (payload.dueRule !== undefined && payload.dueAt === undefined && task.eventId) { + const event = await models.Event.findById(task.eventId).lean(); + task.dueAt = computeDueAtForTask(task, event, null); + } + + await task.save(); + return res.status(200).json({ + success: true, + message: 'Task updated', + data: { task } + }); + } catch (error) { + console.error('Error updating hub task:', error); + return res.status(500).json({ success: false, message: 'Error updating task', error: error.message }); + } +}); + +// Delete organization-level task by id +router.delete('/:orgId/tasks/hub/:taskId', verifyToken, requireEventManagement('orgId'), async (req, res) => { + const { orgId, taskId } = req.params; + const models = getModels(req, 'Task'); + try { + const deleted = await models.Task.findOneAndDelete({ + _id: asObjectId(taskId), + orgId: asObjectId(orgId) + }); + if (!deleted) { + return res.status(404).json({ success: false, message: 'Task not found' }); + } + return res.status(200).json({ success: true, message: 'Task deleted' }); + } catch (error) { + console.error('Error deleting hub task:', error); + return res.status(500).json({ success: false, message: 'Error deleting task', error: error.message }); + } +}); + +// Suggested event tasks based on template/event type +router.get('/:orgId/events/:eventId/task-suggestions', verifyToken, requireEventManagement('orgId'), async (req, res) => { + const { orgId, eventId } = req.params; + const { templateId, eventType } = req.query; + const models = getModels(req, 'Task', 'Event', 'EventTemplate', 'Org'); + try { + const event = await ensureOrgEventAccess(models, orgId, eventId); + if (!event) { + return res.status(404).json({ success: false, message: 'Event not found' }); + } + const statusConfig = await loadOrgTaskBoardConfig(models, orgId); + const librarySuggestions = getSuggestedTasksForEvent(event, { eventType }).map((s) => ({ + ...s, + status: normalizeTaskStatusForOrg(s.status || 'todo', statusConfig) + })); + const template = templateId + ? await models.EventTemplate.findOne({ _id: asObjectId(templateId), orgId: asObjectId(orgId), isActive: true }).lean() + : null; + const templateTasks = Array.isArray(template?.templateData?.taskBlueprint) + ? template.templateData.taskBlueprint.map((task, idx) => ({ + key: task.templateTaskKey || `template_${idx + 1}`, + title: String(task.title || '').trim(), + description: task.description || '', + priority: task.priority || 'medium', + status: normalizeTaskStatusForOrg(task.status || 'todo', statusConfig), + isCritical: Boolean(task.isCritical), + dueRule: task.dueRule || { anchorType: 'none' }, + source: 'template_suggestion', + userConfirmed: false, + templateSource: { + templateId: template?._id || null, + templateTaskKey: task.templateTaskKey || `template_${idx + 1}` + } + })).filter((task) => task.title) + : []; + + return res.status(200).json({ + success: true, + data: { + event: { _id: event._id, name: event.name, type: event.type }, + suggestions: [...librarySuggestions, ...templateTasks] + } + }); + } catch (error) { + console.error('Error loading task suggestions:', error); + return res.status(500).json({ success: false, message: 'Error loading task suggestions', error: error.message }); + } +}); + +// Apply selected suggestions to an event as confirmed tasks +router.post('/:orgId/events/:eventId/tasks/apply-suggestions', verifyToken, requireEventManagement('orgId'), async (req, res) => { + const { orgId, eventId } = req.params; + const payload = req.body || {}; + const models = getModels(req, 'Task', 'Event', 'Org'); + try { + const event = await ensureOrgEventAccess(models, orgId, eventId); + if (!event) { + return res.status(404).json({ success: false, message: 'Event not found' }); + } + const statusConfig = await loadOrgTaskBoardConfig(models, orgId); + const suggestions = Array.isArray(payload.suggestions) ? payload.suggestions : []; + if (!suggestions.length) { + return res.status(400).json({ success: false, message: 'No suggestions provided' }); + } + const approvalAnchorDate = null; + const createdTasks = []; + for (const suggestion of suggestions) { + const title = String(suggestion?.title || '').trim(); + if (!title) continue; + const task = new models.Task({ + orgId: asObjectId(orgId), + eventId: asObjectId(eventId), + title, + description: suggestion.description || '', + status: normalizeTaskStatusForOrg(suggestion.status || 'todo', statusConfig), + priority: suggestion.priority || 'medium', + isCritical: Boolean(suggestion.isCritical), + source: suggestion.source || 'template_applied', + userConfirmed: true, + dueRule: suggestion.dueRule || { anchorType: 'none' }, + templateSource: suggestion.templateSource || undefined + }); + task.dueAt = computeDueAtForTask(task, event, approvalAnchorDate); + await task.save(); + createdTasks.push(task); + } + return res.status(201).json({ + success: true, + message: 'Suggested tasks applied', + data: { createdCount: createdTasks.length, tasks: createdTasks } + }); + } catch (error) { + console.error('Error applying task suggestions:', error); + return res.status(500).json({ success: false, message: 'Error applying task suggestions', error: error.message }); + } +}); + +module.exports = router; diff --git a/backend/routes/userRoutes.js b/backend/routes/userRoutes.js index 2dd242ec..9a207c06 100644 --- a/backend/routes/userRoutes.js +++ b/backend/routes/userRoutes.js @@ -21,30 +21,179 @@ const upload = multer({ }, }); +/** Driver / Mongoose versions differ: code may be number or string, or only message (E11000). */ +function isMongoDuplicateKeyError(err) { + if (!err) return false; + const c = err.code; + if (c === 11000 || c === '11000') return true; + if (err.name === 'MongoServerError' && (c === 11000 || c === '11000')) return true; + if (String(err.message || '').includes('E11000') && String(err.message || '').includes('duplicate key')) { + return true; + } + if (err.cause && isMongoDuplicateKeyError(err.cause)) return true; + return false; +} + +function duplicateIndexFieldFromError(err) { + const root = err.cause && (err.cause.keyPattern || err.cause.keyValue) ? err.cause : err; + if (root.keyPattern && typeof root.keyPattern === 'object') { + const keys = Object.keys(root.keyPattern); + if (keys.length) return keys[0]; + } + const msg = String(err.message || ''); + if (/username_1|dup key:\s*\{\s*username:/i.test(msg)) return 'username'; + if (/email_1|dup key:\s*\{\s*email:/i.test(msg)) return 'email'; + if (/affiliatedEmail/i.test(msg) && /dup key/i.test(msg)) return 'affiliatedEmail'; + return null; +} const router = express.Router(); router.post("/update-user", verifyToken, async (req, res) => { const { User } = getModels(req, 'User'); - const { name, username, classroom, recommendation, onboarded } = req.body + const { name, username, classroom, recommendation, onboarded, tags, onboardingResponses, onboardingCompletedSteps } = req.body; try { const user = await User.findById(req.user.userId); if (!user) { - console.log(`POST: /update-user token is invalid`) - return res.status(404).json({ success: false, message: 'User not found' }); + console.log(`POST: /update-user token is invalid`); + return res.status(404).json({ success: false, message: 'User not found', code: 'USER_NOT_FOUND' }); + } + + if (Object.prototype.hasOwnProperty.call(req.body, 'name')) { + const trimmedName = String(name ?? '').trim(); + if (!trimmedName) { + return res.status(400).json({ + success: false, + message: 'Please enter your name', + code: 'NAME_REQUIRED', + field: 'name', + }); + } + if (trimmedName.length > 120) { + return res.status(400).json({ + success: false, + message: 'Name must be 120 characters or less', + code: 'NAME_TOO_LONG', + field: 'name', + }); + } + user.name = trimmedName; + } + + if (Object.prototype.hasOwnProperty.call(req.body, 'username')) { + const trimmedUsername = String(username ?? '').trim(); + if (!trimmedUsername) { + return res.status(400).json({ + success: false, + message: 'Please choose a username', + code: 'USERNAME_REQUIRED', + field: 'username', + }); + } + if (trimmedUsername.length < 3) { + return res.status(400).json({ + success: false, + message: 'Username must be at least 3 characters', + code: 'USERNAME_TOO_SHORT', + field: 'username', + }); + } + if (trimmedUsername.length > 20) { + return res.status(400).json({ + success: false, + message: 'Username must be 20 characters or less', + code: 'USERNAME_TOO_LONG', + field: 'username', + }); + } + if (!/^[a-zA-Z0-9]+$/.test(trimmedUsername)) { + return res.status(400).json({ + success: false, + message: 'Username can only use letters and numbers (no spaces or symbols)', + code: 'USERNAME_INVALID', + field: 'username', + }); + } + if (isProfane(trimmedUsername)) { + return res.status(400).json({ + success: false, + message: 'Username does not abide by community standards', + code: 'USERNAME_PROFANE', + field: 'username', + }); + } + const escaped = trimmedUsername.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const conflict = await User.findOne({ + username: { $regex: new RegExp(`^${escaped}$`, 'i') }, + _id: { $ne: user._id }, + }); + if (conflict) { + return res.status(400).json({ + success: false, + message: 'This username is already taken', + code: 'USERNAME_TAKEN', + field: 'username', + }); + } + user.username = trimmedUsername; } - user.name = name ? name : user.name; - user.username = username ? username : user.username; + user.classroomPreferences = classroom ? classroom : user.classroomPreferences; user.recommendationPreferences = recommendation ? recommendation : user.recommendationPreferences; user.onboarded = onboarded ? onboarded : user.onboarded; + if (Array.isArray(tags)) { + user.tags = tags + .map((t) => String(t || '').trim()) + .filter(Boolean) + .slice(0, 30); + } + if (onboardingResponses && typeof onboardingResponses === 'object' && !Array.isArray(onboardingResponses)) { + user.onboardingResponses = onboardingResponses; + } + if (Array.isArray(onboardingCompletedSteps)) { + user.onboardingCompletedSteps = onboardingCompletedSteps + .map((stepId) => String(stepId || '').trim()) + .filter(Boolean) + .slice(0, 300); + } await user.save(); console.log(`POST: /update-user ${req.user.userId} successful`); return res.status(200).json({ success: true, message: 'User updated successfully' }); } catch (error) { - console.log(`POST: /update-user ${req.user.userId} failed`) - return res.status(500).json({ success: false, message: error.message }); + console.log(`POST: /update-user ${req.user.userId} failed`, error?.message || error); + if (isMongoDuplicateKeyError(error)) { + const dupField = duplicateIndexFieldFromError(error); + if (dupField === 'username') { + return res.status(400).json({ + success: false, + message: 'This username is already taken', + code: 'USERNAME_TAKEN', + field: 'username', + }); + } + return res.status(400).json({ + success: false, + message: 'This value is already in use', + code: 'DUPLICATE_KEY', + }); + } + if (error.name === 'ValidationError') { + const keys = Object.keys(error.errors || {}); + const firstKey = keys[0]; + const firstMsg = firstKey ? error.errors[firstKey].message : error.message; + return res.status(400).json({ + success: false, + message: firstMsg || 'Invalid profile data', + code: 'VALIDATION_ERROR', + field: firstKey || undefined, + }); + } + return res.status(500).json({ + success: false, + message: 'Something went wrong. Please try again.', + code: 'SERVER_ERROR', + }); } }); @@ -78,14 +227,17 @@ router.post("/check-in", verifyToken, async (req, res) => { const { classroomId } = req.body; try { //check if user is checked in elsewhere in the checked_in array - const classrooms = await Classroom.find({ checked_in: { $in: [req.user.userId] } }); + const classrooms = await Classroom.find({ checked_in: { $in: [req.user.userId] } }).populate( + 'building', + 'name' + ); // const classrooms = await Classroom.find({ checkIns: req.user.userId }); if (classrooms.length > 0) { console.log(`POST: /check-in ${req.user.userId} is already checked in`) return res.status(400).json({ success: false, message: 'User is already checked in' }); } - const classroom = await Classroom.findOne({ _id: classroomId }); + const classroom = await Classroom.findOne({ _id: classroomId }).populate('building', 'name'); classroom.checked_in.push(req.user.userId); await classroom.save(); if (req.user.userId !== "65f474445dca7aca4fb5acaf") { @@ -134,7 +286,10 @@ router.post("/check-in", verifyToken, async (req, res) => { router.get("/checked-in", verifyToken, async (req, res) => { const { Classroom } = getModels(req, 'Classroom'); try { - const classrooms = await Classroom.find({ checked_in: { $in: [req.user.userId] } }); + const classrooms = await Classroom.find({ checked_in: { $in: [req.user.userId] } }).populate( + 'building', + 'name' + ); console.log(`GET: /checked-in ${req.user.userId} successful`) return res.status(200).json({ success: true, message: 'Checked in classrooms retrieved', classrooms }); } catch (error) { @@ -147,7 +302,7 @@ router.post("/check-out", verifyToken, async (req, res) => { const { Classroom, Schedule, User, StudyHistory } = getModels(req, 'Classroom', 'Schedule', 'User', 'StudyHistory'); const { classroomId } = req.body; try { - const classroom = await Classroom.findOne({ _id: classroomId }); + const classroom = await Classroom.findOne({ _id: classroomId }).populate('building', 'name'); classroom.checked_in = classroom.checked_in.filter(userId => userId !== req.user.userId); await classroom.save(); const schedule = await Schedule.findOne({ classroom_id: classroomId }); diff --git a/backend/schemas/classroom.js b/backend/schemas/classroom.js index b4020a61..57fa6f08 100644 --- a/backend/schemas/classroom.js +++ b/backend/schemas/classroom.js @@ -29,9 +29,10 @@ const classroomSchema = new Schema({ default:true, required:true }, - building:{ - type:String, - required:false + building: { + type: Schema.Types.ObjectId, + ref: 'Building', + required: false, }, booking_link: { type: String, diff --git a/backend/schemas/eventTemplate.js b/backend/schemas/eventTemplate.js index d73d21c6..a9abfbb2 100644 --- a/backend/schemas/eventTemplate.js +++ b/backend/schemas/eventTemplate.js @@ -1,60 +1,117 @@ const mongoose = require('mongoose'); const Schema = mongoose.Schema; -const eventTemplateSchema = new mongoose.Schema({ - name: { - type: String, - required: true, - trim: true - }, - description: { - type: String, - required: false - }, - orgId: { - type: Schema.Types.ObjectId, - required: true, - ref: 'Org' - }, - createdBy: { - type: Schema.Types.ObjectId, - required: true, - ref: 'User' +const taskBlueprintDueRuleSchema = new Schema( + { + anchorType: { + type: String, + enum: ['event_start', 'event_end', 'approval_granted', 'absolute', 'none'], + default: 'none' + }, + offsetValue: { type: Number, default: 0 }, + offsetUnit: { + type: String, + enum: ['minutes', 'hours', 'days', 'weeks'], + default: 'days' + }, + direction: { + type: String, + enum: ['before', 'after'], + default: 'before' + } }, - templateData: { - type: { - name: String, + { _id: false } +); + +const taskBlueprintItemSchema = new Schema( + { + templateTaskKey: String, + title: String, + description: String, + priority: { type: String, - location: String, - description: String, - expectedAttendance: Number, - visibility: String, - contact: String, - rsvpEnabled: Boolean, - rsvpRequired: Boolean, - maxAttendees: Number, - externalLink: String - }, - required: true + enum: ['low', 'medium', 'high', 'critical'], + default: 'medium' + }, + status: { + type: String, + enum: ['todo', 'in_progress', 'done', 'cancelled'], + default: 'todo' + }, + isCritical: { + type: Boolean, + default: false + }, + dueRule: taskBlueprintDueRuleSchema }, - isActive: { - type: Boolean, - default: true + { _id: false } +); + +// Nested Schema avoids Mongoose interpreting inner `type: String` as the parent field's type +// (which orphaned `required: true` and caused "true is not a valid type at path required"). +const templateDataSchema = new Schema( + { + name: String, + type: { type: String }, + location: String, + description: String, + expectedAttendance: Number, + visibility: String, + contact: String, + rsvpEnabled: Boolean, + rsvpRequired: Boolean, + maxAttendees: Number, + externalLink: String, + taskBlueprint: [taskBlueprintItemSchema] }, - usageCount: { - type: Number, - default: 0 + { _id: false } +); + +const eventTemplateSchema = new mongoose.Schema( + { + name: { + type: String, + required: true, + trim: true + }, + description: { + type: String, + required: false + }, + orgId: { + type: Schema.Types.ObjectId, + required: true, + ref: 'Org' + }, + createdBy: { + type: Schema.Types.ObjectId, + required: true, + ref: 'User' + }, + templateData: { + type: templateDataSchema, + required: true + }, + isActive: { + type: Boolean, + default: true + }, + usageCount: { + type: Number, + default: 0 + }, + lastUsed: { + type: Date, + default: null + } }, - lastUsed: { - type: Date, - default: null + { + timestamps: true } -}, { - timestamps: true -}); +); // Index for efficient queries eventTemplateSchema.index({ orgId: 1, isActive: 1 }); eventTemplateSchema.index({ createdBy: 1 }); -module.exports = mongoose.model('EventTemplate', eventTemplateSchema); +module.exports = eventTemplateSchema; diff --git a/backend/schemas/financeConfig.js b/backend/schemas/financeConfig.js new file mode 100644 index 00000000..dc970ad7 --- /dev/null +++ b/backend/schemas/financeConfig.js @@ -0,0 +1,52 @@ +const mongoose = require('mongoose'); + +const lineItemDefinitionSchema = new mongoose.Schema( + { + key: { type: String, required: true }, + label: { type: String, required: true }, + required: { type: Boolean, default: false }, + kind: { type: String, enum: ['currency', 'number', 'text'], default: 'currency' }, + helpText: { type: String, default: '' } + }, + { _id: false } +); + +const workflowStageSchema = new mongoose.Schema( + { + key: { type: String, required: true }, + label: { type: String, required: true }, + actorType: { type: String, enum: ['org_permission', 'platform_admin'], required: true }, + permission: { type: String, default: 'manage_finances' } + }, + { _id: false } +); + +const workflowPresetSchema = new mongoose.Schema( + { + presetKey: { type: String, required: true }, + stages: { type: [workflowStageSchema], default: [] } + }, + { _id: false } +); + +const budgetTemplateSchema = new mongoose.Schema( + { + templateKey: { type: String, required: true }, + displayName: { type: String, required: true }, + orgTypeKeys: { type: [String], default: [] }, + fiscalLabel: { type: String, default: 'Fiscal year' }, + workflowPresetKey: { type: String, default: 'two_stage' }, + lineItemDefinitions: { type: [lineItemDefinitionSchema], default: [] } + }, + { _id: false } +); + +const financeConfigSchema = new mongoose.Schema( + { + budgetTemplates: { type: [budgetTemplateSchema], default: [] }, + workflowPresets: { type: [workflowPresetSchema], default: [] } + }, + { timestamps: true } +); + +module.exports = financeConfigSchema; diff --git a/backend/schemas/org.js b/backend/schemas/org.js index ee3471ee..3937176b 100644 --- a/backend/schemas/org.js +++ b/backend/schemas/org.js @@ -277,6 +277,54 @@ const OrgSchema= new Schema({ type: Boolean, default: false }, + /** Operational lifecycle (distinct from verificationStatus / approvalStatus). See atlasPolicy in OrgManagementConfig. */ + lifecycleStatus: { + type: String, + default: 'active', + trim: true + }, + orgTypeKey: { + type: String, + trim: true, + default: undefined + }, + lifecycleChangedAt: { + type: Date + }, + lifecycleChangedBy: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + adminNotes: { + type: String, + maxlength: 4000 + }, + governanceDocuments: { + type: [{ + key: { + type: String, + required: true, + trim: true + }, + versions: [{ + version: { type: Number, required: true }, + storageUrl: { type: String, required: true }, + originalFilename: { type: String }, + mimeType: { type: String }, + uploadedBy: { type: Schema.Types.ObjectId, ref: 'User' }, + uploadedAt: { type: Date, default: Date.now }, + effectiveFrom: { type: Date }, + status: { + type: String, + enum: ['draft', 'approved', 'superseded'], + default: 'draft' + }, + approvedBy: { type: Schema.Types.ObjectId, ref: 'User' }, + approvedAt: { type: Date } + }] + }], + default: [] + }, // Soft delete isDeleted: { type: Boolean, @@ -290,6 +338,33 @@ const OrgSchema= new Schema({ type: Schema.Types.ObjectId, ref: 'User', default: null + }, + /** Custom task hub / event task Kanban columns (max 10). Empty/absent = platform defaults. */ + taskBoardStatuses: { + type: [{ + key: { + type: String, + required: true, + trim: true, + lowercase: true + }, + label: { + type: String, + required: true, + trim: true, + maxlength: 64 + }, + category: { + type: String, + enum: ['backlog', 'active', 'done', 'cancelled'], + required: true + }, + order: { + type: Number, + default: 0 + } + }], + default: undefined } }); @@ -363,6 +438,9 @@ OrgSchema.methods.hasPermission = function(roleName, permission) { if (role.permissions.includes('all')) return true; if (role.permissions.includes(permission)) return true; + // Finance: managing implies viewing (roles often grant manage_finances only) + if (permission === 'view_finances' && role.permissions.includes('manage_finances')) return true; + // Also check boolean flags (roles may have canManageEvents etc. without permissions array) if (permission === 'manage_events' && role.canManageEvents) return true; if (permission === 'manage_members' && role.canManageMembers) return true; @@ -462,4 +540,24 @@ OrgSchema.statics.createApprovalGroup = function(name, displayName, description, }); }; +OrgSchema.methods.getGovernanceSlot = function (key) { + const docs = this.governanceDocuments || []; + return docs.find((d) => d.key === key); +}; + +OrgSchema.methods.addGovernanceVersion = function (key, versionPayload) { + if (!this.governanceDocuments) { + this.governanceDocuments = []; + } + let slot = this.governanceDocuments.find((d) => d.key === key); + if (!slot) { + this.governanceDocuments.push({ key, versions: [] }); + slot = this.governanceDocuments[this.governanceDocuments.length - 1]; + } + const versions = slot.versions || []; + const nextVer = versions.length ? Math.max(...versions.map((v) => v.version)) + 1 : 1; + slot.versions.push({ ...versionPayload, version: nextVer }); + return nextVer; +}; + module.exports=OrgSchema; diff --git a/backend/schemas/orgBudget.js b/backend/schemas/orgBudget.js new file mode 100644 index 00000000..992b93f2 --- /dev/null +++ b/backend/schemas/orgBudget.js @@ -0,0 +1,104 @@ +const mongoose = require('mongoose'); + +const lineItemValueSchema = new mongoose.Schema( + { + key: { type: String, required: true }, + label: { type: String, default: '' }, + kind: { type: String, enum: ['currency', 'number', 'text'], default: 'currency' }, + amount: { type: Number, default: null }, + numberValue: { type: Number, default: null }, + textValue: { type: String, default: '' }, + note: { type: String, default: '' } + }, + { _id: false } +); + +const workflowStageStateSchema = new mongoose.Schema( + { + key: { type: String, required: true }, + label: { type: String, default: '' }, + actorType: { type: String, enum: ['org_permission', 'platform_admin'], required: true }, + permission: { type: String, default: '' } + }, + { _id: false } +); + +const completedStageSchema = new mongoose.Schema( + { + key: { type: String, required: true }, + approvedAt: { type: Date, default: Date.now }, + approvedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' } + }, + { _id: false } +); + +const budgetCommentSchema = new mongoose.Schema( + { + userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, + body: { type: String, required: true }, + createdAt: { type: Date, default: Date.now }, + revision: { type: Number, default: null } + }, + { _id: false } +); + +const budgetRevisionSchema = new mongoose.Schema( + { + revision: { type: Number, required: true }, + createdAt: { type: Date, default: Date.now }, + createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, + lineItemsSnapshot: { type: [mongoose.Schema.Types.Mixed], default: [] }, + workflowSnapshot: { type: mongoose.Schema.Types.Mixed, default: {} }, + status: { type: String, default: '' } + }, + { _id: false } +); + +const budgetAuditEntrySchema = new mongoose.Schema( + { + at: { type: Date, default: Date.now }, + userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, + actor: { type: String, enum: ['org', 'platform', 'system'], required: true }, + action: { type: String, required: true }, + message: { type: String, default: '' }, + fromStatus: { type: String, default: '' }, + toStatus: { type: String, default: '' }, + stageKey: { type: String, default: '' } + }, + { _id: false } +); + +const orgBudgetSchema = new mongoose.Schema( + { + orgId: { type: mongoose.Schema.Types.ObjectId, ref: 'Org', required: true, index: true }, + templateKey: { type: String, required: true }, + fiscalYear: { type: String, required: true }, + title: { type: String, default: '' }, + status: { + type: String, + enum: ['draft', 'submitted', 'in_review', 'approved', 'rejected', 'revision_requested'], + default: 'draft', + index: true + }, + lineItems: { type: [lineItemValueSchema], default: [] }, + workflow: { + presetKey: { type: String, default: '' }, + currentStageIndex: { type: Number, default: 0 }, + stagesSnapshot: { type: [workflowStageStateSchema], default: [] }, + completedStages: { type: [completedStageSchema], default: [] }, + lastActionAt: { type: Date }, + lastActionBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' } + }, + revisions: { type: [budgetRevisionSchema], default: [] }, + comments: { type: [budgetCommentSchema], default: [] }, + auditLog: { type: [budgetAuditEntrySchema], default: [] }, + createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, + updatedBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' } + }, + { timestamps: true } +); + +orgBudgetSchema.index({ orgId: 1, fiscalYear: 1, templateKey: 1 }); +orgBudgetSchema.index({ orgId: 1, status: 1 }); + +module.exports = orgBudgetSchema; diff --git a/backend/schemas/orgManagementConfig.js b/backend/schemas/orgManagementConfig.js index 17d783df..b0e82a36 100644 --- a/backend/schemas/orgManagementConfig.js +++ b/backend/schemas/orgManagementConfig.js @@ -149,6 +149,13 @@ const orgManagementConfigSchema = new mongoose.Schema({ default: false }, + /** Root operator dashboard: classic (full root + Compass/Atlas/Beacon) vs engagement_hub (single consolidated shell). */ + operatorDashboardMode: { + type: String, + enum: ['classic', 'engagement_hub'], + default: 'classic' + }, + // Atlas org approval config orgApproval: { mode: { @@ -409,6 +416,70 @@ const orgManagementConfigSchema = new mongoose.Schema({ default: true } } + }, + + /** CMS Phase 1: tenant/org-type lifecycle, governance terminology, directory rules (see atlasPolicyService defaults). */ + atlasPolicy: { + type: mongoose.Schema.Types.Mixed, + default: undefined + }, + + /** User onboarding shown after signup/login (tenant configurable, root-managed). */ + userOnboarding: { + enabled: { + type: Boolean, + default: false + }, + welcomeTitle: { + type: String, + default: 'Welcome to your community' + }, + welcomeSubtitle: { + type: String, + default: 'A quick setup helps community managers and campus admins better support your interests.' + }, + collectName: { + type: Boolean, + default: true + }, + collectInterests: { + type: Boolean, + default: true + }, + enforceMinInterests: { + type: Boolean, + default: true + }, + enforceMaxInterests: { + type: Boolean, + default: true + }, + minInterests: { + type: Number, + default: 1 + }, + maxInterests: { + type: Number, + default: 6 + }, + customSteps: { + type: [ + { + id: { type: String, required: true }, + label: { type: String, required: true }, + type: { + type: String, + enum: ['short-text', 'long-text', 'single-select', 'multi-select'], + default: 'short-text' + }, + required: { type: Boolean, default: false }, + options: { type: [String], default: [] }, + placeholder: { type: String, default: '' }, + helpText: { type: String, default: '' } + } + ], + default: [] + } } }, { timestamps: true }); diff --git a/backend/schemas/orgMember.js b/backend/schemas/orgMember.js index c5fa6d1b..89e5d4e9 100644 --- a/backend/schemas/orgMember.js +++ b/backend/schemas/orgMember.js @@ -35,6 +35,14 @@ const memberSchema = new Schema({ type: Date, default: Date.now }, + roleTermStart: { + type: Date, + required: false + }, + roleTermEnd: { + type: Date, + required: false + }, // For tracking role changes roleHistory: [{ role: String, @@ -49,6 +57,22 @@ const memberSchema = new Schema({ reason: String }], + membershipHistory: [{ + at: { type: Date, default: Date.now }, + action: { + type: String, + required: true + }, + fromStatus: String, + toStatus: String, + role: String, + actorUserId: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + reason: String + }], + // Custom permissions that override role permissions customPermissions: { type: [String], @@ -118,20 +142,33 @@ memberSchema.methods.canViewAnalytics = async function(org) { }; // Method to change role with history tracking -memberSchema.methods.changeRole = async function(newRole, assignedBy, reason = '') { - // Add to history +memberSchema.methods.changeRole = async function(newRole, assignedBy, reason = '', options = {}) { + const { termStart, termEnd } = options || {}; this.roleHistory.push({ role: this.role, assignedBy: this.assignedBy, assignedAt: this.assignedAt, reason: reason }); - - // Update current role + if (!this.membershipHistory) { + this.membershipHistory = []; + } + this.membershipHistory.push({ + at: new Date(), + action: 'role_change', + role: newRole, + actorUserId: assignedBy, + reason: reason || '' + }); this.role = newRole; this.assignedBy = assignedBy; this.assignedAt = new Date(); - + if (termStart !== undefined) { + this.roleTermStart = termStart; + } + if (termEnd !== undefined) { + this.roleTermEnd = termEnd; + } return this.save(); }; diff --git a/backend/schemas/orgMembershipAudit.js b/backend/schemas/orgMembershipAudit.js new file mode 100644 index 00000000..e0be687f --- /dev/null +++ b/backend/schemas/orgMembershipAudit.js @@ -0,0 +1,16 @@ +const mongoose = require('mongoose'); + +/** Append-only audit when OrgMember document is hard-deleted (removal history). */ +const orgMembershipAuditSchema = new mongoose.Schema({ + org_id: { type: mongoose.Schema.Types.ObjectId, ref: 'Org', required: true, index: true }, + user_id: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, index: true }, + action: { type: String, required: true }, + actorUserId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' }, + reason: { type: String }, + meta: { type: mongoose.Schema.Types.Mixed }, + at: { type: Date, default: Date.now } +}, { timestamps: false }); + +orgMembershipAuditSchema.index({ org_id: 1, at: -1 }); + +module.exports = orgMembershipAuditSchema; diff --git a/backend/schemas/task.js b/backend/schemas/task.js new file mode 100644 index 00000000..31fe9728 --- /dev/null +++ b/backend/schemas/task.js @@ -0,0 +1,223 @@ +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const taskDueRuleSchema = new Schema({ + anchorType: { + type: String, + enum: ['event_start', 'event_end', 'approval_granted', 'absolute', 'none'], + default: 'none' + }, + offsetValue: { + type: Number, + default: 0 + }, + offsetUnit: { + type: String, + enum: ['minutes', 'hours', 'days', 'weeks'], + default: 'days' + }, + direction: { + type: String, + enum: ['before', 'after'], + default: 'before' + }, + absoluteDate: { + type: Date, + default: null + } +}, { _id: false }); + +const taskBlockerSchema = new Schema({ + type: { + type: String, + enum: ['approval', 'booking', 'task', 'dependency', 'manual'], + default: 'manual' + }, + referenceId: { + type: String, + default: null + }, + label: { + type: String, + trim: true, + default: '' + }, + resolved: { + type: Boolean, + default: false + } +}, { _id: false }); + +const taskIntegrationSchema = new Schema({ + type: { + type: String, + enum: ['approval_instance', 'room_booking', 'registration_goal', 'agenda_item', 'event_job'], + required: true + }, + referenceId: { + type: String, + required: true + }, + status: { + type: String, + default: 'pending' + } +}, { _id: false }); + +const TaskSchema = new Schema({ + orgId: { + type: Schema.Types.ObjectId, + ref: 'Org', + required: true, + index: true + }, + eventId: { + type: Schema.Types.ObjectId, + ref: 'Event', + required: false, + default: null, + index: true + }, + title: { + type: String, + required: true, + trim: true, + maxlength: 180 + }, + description: { + type: String, + trim: true, + default: '' + }, + status: { + type: String, + trim: true, + maxlength: 48, + default: 'todo', + index: true + }, + /** Manual order within the same status column (Task Hub / event task board). */ + boardRank: { + type: Number, + default: 0 + }, + priority: { + type: String, + enum: ['low', 'medium', 'high', 'critical'], + default: 'medium', + index: true + }, + isCritical: { + type: Boolean, + default: false + }, + ownerUserId: { + type: Schema.Types.ObjectId, + ref: 'User', + default: null, + index: true + }, + watcherUserIds: [{ + type: Schema.Types.ObjectId, + ref: 'User' + }], + source: { + type: String, + enum: ['manual', 'template_suggestion', 'template_applied', 'integration_generated'], + default: 'manual' + }, + userConfirmed: { + type: Boolean, + default: false + }, + templateSource: { + templateId: { + type: Schema.Types.ObjectId, + default: null + }, + templateTaskKey: { + type: String, + default: null + } + }, + dueRule: { + type: taskDueRuleSchema, + default: () => ({ anchorType: 'none' }) + }, + dueAt: { + type: Date, + default: null, + index: true + }, + blockers: { + type: [taskBlockerSchema], + default: [] + }, + integrationLinks: { + type: [taskIntegrationSchema], + default: [] + }, + tags: { + type: [String], + default: [] + }, + readinessContribution: { + weight: { + type: Number, + default: 1, + min: 0 + }, + blocked: { + type: Boolean, + default: false + } + }, + completedAt: { + type: Date, + default: null + }, + cancelledAt: { + type: Date, + default: null + }, + metadata: { + type: Schema.Types.Mixed, + default: {} + } +}, { + timestamps: true +}); + +TaskSchema.index({ orgId: 1, eventId: 1, status: 1 }); +TaskSchema.index({ orgId: 1, dueAt: 1 }); +TaskSchema.index({ orgId: 1, ownerUserId: 1, status: 1 }); + +TaskSchema.pre('save', async function syncCompletionTimestamps() { + const { getResolvedTaskBoardStatuses, resolveStatusCategory } = require('../services/taskBoardStatusUtils'); + try { + const Org = this.db.model('Org'); + const org = await Org.findById(this.orgId).select('taskBoardStatuses').lean(); + const cfg = getResolvedTaskBoardStatuses(org); + const cat = resolveStatusCategory(this.status, cfg); + if (cat === 'done' && !this.completedAt) { + this.completedAt = new Date(); + } + if (cat !== 'done' && this.completedAt) { + this.completedAt = null; + } + if (cat === 'cancelled' && !this.cancelledAt) { + this.cancelledAt = new Date(); + } + if (cat !== 'cancelled' && this.cancelledAt) { + this.cancelledAt = null; + } + } catch (_err) { + const st = String(this.status || ''); + if (st === 'done' && !this.completedAt) this.completedAt = new Date(); + if (st !== 'done' && this.completedAt) this.completedAt = null; + if (st === 'cancelled' && !this.cancelledAt) this.cancelledAt = new Date(); + if (st !== 'cancelled' && this.cancelledAt) this.cancelledAt = null; + } +}); + +module.exports = TaskSchema; diff --git a/backend/schemas/user.js b/backend/schemas/user.js index d5ec5d04..72628527 100644 --- a/backend/schemas/user.js +++ b/backend/schemas/user.js @@ -113,6 +113,14 @@ const userSchema = new mongoose.Schema({ type:Array, default: [], }, + onboardingResponses: { + type: mongoose.Schema.Types.Mixed, + default: {} + }, + onboardingCompletedSteps: { + type: [String], + default: [] + }, developer: { type: Number, default: 0, @@ -210,6 +218,15 @@ const userSchema = new mongoose.Schema({ default: null, trim: true }, + /** When true, password login and API access (verifyToken) are blocked for this tenant user. */ + accessSuspended: { + type: Boolean, + default: false, + }, + accessSuspendedAt: { + type: Date, + default: null, + }, // you can add more fields here if needed, like 'createdAt', 'updatedAt', etc. diff --git a/backend/services/adminTenantEventOperatorService.js b/backend/services/adminTenantEventOperatorService.js new file mode 100644 index 00000000..36f9eb6d --- /dev/null +++ b/backend/services/adminTenantEventOperatorService.js @@ -0,0 +1,255 @@ +/** + * Tenant admin (admin/developer/beta) event read APIs without org-event-management membership. + * Uses getModels(req, ...) per backend best practices. + */ + +const getModels = require('./getModelService'); + +function defaultAnalytics() { + return { + views: 0, + uniqueViews: 0, + registrations: 0, + uniqueRegistrations: 0, + engagementRate: 0, + }; +} + +function resolveEffectiveOrgId(event) { + if (!event || event.hostingType !== 'Org' || !event.hostingId) return null; + const hid = event.hostingId; + return hid._id != null ? String(hid._id) : String(hid); +} + +/** + * @param {import('express').Request} req + * @param {string} eventId + */ +async function getAdminTenantEventDashboard(req, eventId) { + const { Event, EventAnalytics, EventAgenda, EventJob, VolunteerSignup, EventEquipment } = getModels( + req, + 'Event', + 'EventAnalytics', + 'EventAgenda', + 'EventJob', + 'VolunteerSignup', + 'EventEquipment' + ); + + const event = await Event.findOne({ + _id: eventId, + isDeleted: { $ne: true }, + }) + .populate('hostingId', 'org_name org_profile_image name username email') + .populate('collaboratorOrgs.orgId', 'org_name org_profile_image'); + + if (!event) { + return null; + } + + const analyticsDoc = await EventAnalytics.findOne({ eventId }); + const analytics = analyticsDoc ? analyticsDoc.toObject() : defaultAnalytics(); + if (analytics.engagementRate == null) analytics.engagementRate = 0; + + const agenda = await EventAgenda.findOne({ eventId }); + const roles = await EventJob.find({ eventId }); + const totalVolunteers = roles.reduce((sum, role) => sum + (role.assignments?.length || 0), 0); + const confirmedVolunteers = roles.reduce( + (sum, role) => sum + (role.assignments?.filter((a) => a.status === 'confirmed')?.length || 0), + 0 + ); + const signups = await VolunteerSignup.find({ eventId }).populate('memberId', 'name email'); + const equipment = await EventEquipment.findOne({ eventId }); + + const registrationCount = event.registrationCount ?? (event.attendees?.length ?? 0); + const checkedInCount = signups.filter((s) => s.checkedIn).length; + + let eventCheckIn = null; + if (event.checkInEnabled && event.attendees && Array.isArray(event.attendees)) { + const totalCheckedIn = event.attendees.filter((a) => a.checkedIn).length; + const totalRegistrations = event.registrationCount ?? event.attendees.length; + eventCheckIn = { + totalCheckedIn, + totalRegistrations, + checkInRate: totalRegistrations > 0 ? ((totalCheckedIn / totalRegistrations) * 100).toFixed(1) : '0', + }; + } + + const now = new Date(); + let operationalStatus = 'upcoming'; + if (event.start_time <= now && event.end_time >= now) { + operationalStatus = 'active'; + } else if (event.end_time < now) { + operationalStatus = 'completed'; + } + + const effectiveOrgId = resolveEffectiveOrgId(event); + + return { + event, + analytics, + agenda: agenda || { items: [] }, + roles: { + total: roles.length, + assignments: totalVolunteers, + confirmed: confirmedVolunteers, + signups: signups.length, + }, + equipment: equipment || { items: [] }, + stats: { + registrationCount, + volunteers: { + total: totalVolunteers, + confirmed: confirmedVolunteers, + checkedIn: checkedInCount, + }, + operationalStatus, + checkIn: eventCheckIn, + }, + effectiveOrgId, + }; +} + +/** + * @param {import('express').Request} req + * @param {string} eventId + */ +async function getAdminTenantEventRegistrationResponses(req, eventId) { + const { Event, FormResponse } = getModels(req, 'Event', 'FormResponse'); + + const event = await Event.findOne({ _id: eventId, isDeleted: { $ne: true } }).populate( + 'attendees.userId', + 'name username email' + ); + + if (!event) { + return null; + } + + const registrations = (event.attendees || []) + .filter((a) => !a.walkIn) + .map((a) => ({ + userId: a.userId, + registeredAt: a.registeredAt, + guestCount: a.guestCount, + checkedIn: a.checkedIn, + checkedInAt: a.checkedInAt, + })); + + let formResponses = []; + if (event.registrationFormId) { + const responses = await FormResponse.find({ event: eventId }) + .populate('submittedBy', 'name username email picture') + .sort({ submittedAt: 1 }) + .lean(); + formResponses = responses.map((r) => ({ + _id: r._id, + submittedBy: r.submittedBy, + guestName: r.guestName, + guestEmail: r.guestEmail ?? r.guestUsername, + submittedAt: r.submittedAt, + formSnapshot: r.formSnapshot, + answers: r.answers, + })); + } + + return { + registrations, + formResponses, + registrationFormId: event.registrationFormId || null, + }; +} + +/** + * RSVP growth by day (same logic as org-event-management route, scoped by event id only). + * @param {import('express').Request} req + * @param {string} eventId + * @param {{ timezone?: string }} query + */ +async function getAdminTenantEventRsvpGrowth(req, eventId, query = {}) { + const { Event, FormResponse, EventAnalytics } = getModels(req, 'Event', 'FormResponse', 'EventAnalytics'); + + const event = await Event.findOne({ _id: eventId, isDeleted: { $ne: true } }); + + if (!event) { + return null; + } + + const attendees = event.attendees || []; + const registrationCount = event.registrationCount ?? attendees.length; + + const eventStart = new Date(event.start_time); + const eventCreated = new Date(event.createdAt || event.start_time); + const now = new Date(); + const cutoffDate = eventStart < now ? eventStart : now; + const cutoffDateNormalized = new Date(cutoffDate); + cutoffDateNormalized.setHours(23, 59, 59, 999); + + const registrations = {}; + const timezone = query.timezone || 'UTC'; + + function toLocalDateKey(date) { + const d = new Date(date); + const formatter = new Intl.DateTimeFormat('en-CA', { + timeZone: timezone, + year: 'numeric', + month: '2-digit', + day: '2-digit', + }); + return formatter.format(d); + } + + attendees.forEach((attendee) => { + const uid = attendee?.userId ?? attendee?.user; + if (!attendee || !uid) return; + const guestCount = attendee.guestCount || 1; + const regAt = attendee.registeredAt || attendee.rsvpDate; + const regDate = regAt ? new Date(regAt) : new Date(eventCreated); + if (regDate > cutoffDateNormalized) return; + const dayKey = toLocalDateKey(regDate); + if (!registrations[dayKey]) registrations[dayKey] = 0; + registrations[dayKey] += guestCount; + }); + + const formResponses = await FormResponse.find({ + event: eventId, + submittedBy: null, + }) + .select('submittedAt') + .lean(); + + formResponses.forEach((fr) => { + const regDate = fr.submittedAt ? new Date(fr.submittedAt) : new Date(eventCreated); + if (regDate > cutoffDateNormalized) return; + const dayKey = toLocalDateKey(regDate); + if (!registrations[dayKey]) registrations[dayKey] = 0; + registrations[dayKey] += 1; + }); + + const totalFromAttendeesAndForm = Object.values(registrations).reduce((a, b) => a + b, 0); + if (totalFromAttendeesAndForm === 0 && registrationCount > 0) { + const analytics = await EventAnalytics.findOne({ eventId }).select('registrationHistory').lean(); + const history = analytics?.registrationHistory || []; + history.forEach((r) => { + const regDate = r.timestamp ? new Date(r.timestamp) : new Date(eventCreated); + if (regDate > cutoffDateNormalized) return; + const dayKey = toLocalDateKey(regDate); + if (!registrations[dayKey]) registrations[dayKey] = 0; + registrations[dayKey] += 1; + }); + } + + return { + registrations, + eventCreated: eventCreated.toISOString(), + eventStart: eventStart.toISOString(), + expectedAttendance: event.expectedAttendance || 0, + }; +} + +module.exports = { + getAdminTenantEventDashboard, + getAdminTenantEventRegistrationResponses, + getAdminTenantEventRsvpGrowth, + defaultAnalytics, +}; diff --git a/backend/services/adminTenantEventsService.js b/backend/services/adminTenantEventsService.js new file mode 100644 index 00000000..127bce26 --- /dev/null +++ b/backend/services/adminTenantEventsService.js @@ -0,0 +1,148 @@ +/** + * Upcoming / live events for tenant admin views (tenant-scoped, elevated roles). + * Uses getModels(req, ...) per backend best practices. + */ + +const getModels = require('./getModelService'); +const mongoose = require('mongoose'); + +const MAX_LIMIT = 40; + +function escapeRegex(str) { + return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function buildAnalyticsEventIdMatch(eventIds) { + const strIds = [...new Set((eventIds || []).map((id) => String(id)).filter(Boolean))]; + const objIds = strIds + .filter((id) => mongoose.Types.ObjectId.isValid(id)) + .map((id) => new mongoose.Types.ObjectId(id)); + return { + strIds, + match: { + $or: [{ 'properties.event_id': { $in: strIds } }, { 'properties.event_id': { $in: objIds } }], + }, + }; +} + +async function buildAnalyticsByEventId(AnalyticsEvent, eventIds) { + const { strIds, match } = buildAnalyticsEventIdMatch(eventIds); + if (!strIds.length) return new Map(); + + const rows = await AnalyticsEvent.aggregate([ + { + $match: { + event: { $in: ['event_view', 'event_registration'] }, + ...match, + }, + }, + { + $group: { + _id: { eventId: '$properties.event_id', event: '$event' }, + count: { $sum: 1 }, + }, + }, + ]); + + const map = new Map(); + for (const row of rows) { + const eventId = row?._id?.eventId != null ? String(row._id.eventId) : ''; + if (!eventId) continue; + if (!map.has(eventId)) { + map.set(eventId, { + views: 0, + uniqueViews: 0, + registrations: 0, + uniqueRegistrations: 0, + }); + } + const current = map.get(eventId); + if (row?._id?.event === 'event_view') { + current.views = row.count ?? 0; + } else if (row?._id?.event === 'event_registration') { + current.registrations = row.count ?? 0; + } + } + return map; +} + +/** + * @param {import('express').Request} req + * @param {{ page?: number, limit?: number, q?: string, includePast?: boolean }} opts + */ +async function listAdminTenantUpcomingEvents(req, opts = {}) { + const { Event, AnalyticsEvent } = getModels(req, 'Event', 'AnalyticsEvent'); + const rawPage = Math.max(1, parseInt(String(opts.page ?? 1), 10) || 1); + const limit = Math.min(MAX_LIMIT, Math.max(1, parseInt(String(opts.limit ?? 20), 10) || 20)); + + const rawQ = typeof opts.q === 'string' ? opts.q.trim() : ''; + const q = rawQ.length >= 3 ? rawQ : ''; + const includePast = Boolean(opts.includePast); + + const now = new Date(); + const query = { + isDeleted: { $ne: true }, + }; + if (!includePast) { + query.end_time = { $gte: now }; + } + if (q) { + query.name = { $regex: escapeRegex(q), $options: 'i' }; + } + + const sort = includePast ? { start_time: -1 } : { start_time: 1 }; + + const total = await Event.countDocuments(query); + const totalPages = Math.max(1, Math.ceil(total / limit)); + const page = Math.min(rawPage, totalPages); + const skip = (page - 1) * limit; + + const events = await Event.find(query) + .select( + 'name start_time end_time status visibility type hostingType hostingId location expectedAttendance registrationCount' + ) + .sort(sort) + .skip(skip) + .limit(limit) + .lean(); + + const ids = events.map((e) => e._id).filter(Boolean); + let analyticsById = new Map(); + if (ids.length) { + analyticsById = await buildAnalyticsByEventId(AnalyticsEvent, ids); + } + + const enrichedEvents = events.map((e) => { + const a = analyticsById.get(String(e._id)); + return { + ...e, + analyticsSummary: a + ? { + views: a.views ?? 0, + uniqueViews: a.uniqueViews ?? 0, + registrations: a.registrations ?? 0, + uniqueRegistrations: a.uniqueRegistrations ?? 0, + } + : { + views: 0, + uniqueViews: 0, + registrations: 0, + uniqueRegistrations: 0, + }, + }; + }); + + return { + events: enrichedEvents, + pagination: { + total, + page, + limit, + totalPages: Math.max(1, Math.ceil(total / limit)), + }, + }; +} + +module.exports = { + listAdminTenantUpcomingEvents, +}; diff --git a/backend/services/adminTenantSummaryService.js b/backend/services/adminTenantSummaryService.js new file mode 100644 index 00000000..74a5c15d --- /dev/null +++ b/backend/services/adminTenantSummaryService.js @@ -0,0 +1,40 @@ +/** + * Read-only aggregates for community admin dashboard home (institution-scoped). + * Uses getModels(req, ...) per backend best practices. + */ + +const getModels = require('./getModelService'); + +/** + * @param {import('express').Request} req + * @returns {Promise<{ + * communityGroupCount: number, + * upcomingEventsCount: number, + * programsCount: number, + * userCount: number + * }>} + */ +async function getAdminTenantSummary(req) { + const { Org, Event, Domain, User } = getModels(req, 'Org', 'Event', 'Domain', 'User'); + + const startOfToday = new Date(); + startOfToday.setHours(0, 0, 0, 0); + + const [communityGroupCount, upcomingEventsCount, programsCount, userCount] = await Promise.all([ + Org.countDocuments({}), + Event.countDocuments({ start_time: { $gte: startOfToday } }), + Domain.countDocuments({}), + User.countDocuments({}), + ]); + + return { + communityGroupCount, + upcomingEventsCount, + programsCount, + userCount, + }; +} + +module.exports = { + getAdminTenantSummary, +}; diff --git a/backend/services/atlasPolicyService.js b/backend/services/atlasPolicyService.js new file mode 100644 index 00000000..59be787e --- /dev/null +++ b/backend/services/atlasPolicyService.js @@ -0,0 +1,218 @@ +const DEFAULT_ATLAS_POLICY = { + lifecycle: { + statuses: [ + { key: 'active', label: 'Active' }, + { key: 'sunset', label: 'Sunset' }, + { key: 'inactive', label: 'Inactive' } + ], + defaultStatus: 'active', + transitions: [ + { from: 'active', to: 'sunset', allowedActors: ['admin', 'root', 'officer'] }, + { from: 'sunset', to: 'inactive', allowedActors: ['admin', 'root'] }, + { from: 'sunset', to: 'active', allowedActors: ['admin', 'root', 'officer'] }, + { from: 'inactive', to: 'active', allowedActors: ['admin', 'root'] } + ] + }, + orgTypes: [ + { key: 'default', displayName: 'General', requiredGovernanceKeys: ['constitution'] }, + { key: 'club', displayName: 'Club', requiredGovernanceKeys: ['constitution'] } + ], + defaultOrgTypeKey: 'default', + terminology: { + constitution: 'Constitution', + charter: 'Charter', + bylaws: 'Bylaws', + member_list: 'Member list', + financial_statement: 'Financial statement' + }, + directory: { + hideNonActiveFromPublicList: false, + nonActiveStatuses: ['inactive'] + }, + budgets: { + /** template_only: clubs must use preset line-item definitions; template_plus_custom: clubs can append custom rows. */ + lineItemMode: 'template_only', + /** Safety cap for custom rows when lineItemMode=template_plus_custom. */ + maxCustomLineItems: 20 + }, + events: { + inactiveOrgBlocksEventCreation: true, + /** lifecycleStatus values that block creating new org events when policy enabled */ + blockedLifecycleStatuses: ['inactive', 'sunset'] + } +}; + +function deepMergeAtlasPolicy(stored) { + const base = JSON.parse(JSON.stringify(DEFAULT_ATLAS_POLICY)); + if (!stored || typeof stored !== 'object') return base; + return { + lifecycle: { ...base.lifecycle, ...(stored.lifecycle || {}) }, + orgTypes: Array.isArray(stored.orgTypes) && stored.orgTypes.length > 0 + ? stored.orgTypes + : base.orgTypes, + defaultOrgTypeKey: stored.defaultOrgTypeKey || base.defaultOrgTypeKey, + terminology: { ...base.terminology, ...(stored.terminology || {}) }, + directory: { ...base.directory, ...(stored.directory || {}) }, + budgets: { ...base.budgets, ...(stored.budgets || {}) }, + events: { ...base.events, ...(stored.events || {}) } + }; +} + +/** + * Merge transitions and statuses from stored policy without losing defaults entirely + */ +function getEffectivePolicyFromConfig(configDoc) { + const raw = configDoc?.atlasPolicy; + const merged = deepMergeAtlasPolicy(raw); + if (raw?.lifecycle?.transitions?.length) { + merged.lifecycle.transitions = raw.lifecycle.transitions; + } + if (raw?.lifecycle?.statuses?.length) { + merged.lifecycle.statuses = raw.lifecycle.statuses; + } + if (raw?.lifecycle?.defaultStatus) { + merged.lifecycle.defaultStatus = raw.lifecycle.defaultStatus; + } + return merged; +} + +async function getEffectivePolicy(req) { + const getModels = require('./getModelService'); + const { OrgManagementConfig } = getModels(req, 'OrgManagementConfig'); + const config = await OrgManagementConfig.findOne(); + return getEffectivePolicyFromConfig(config); +} + +function statusKeys(policy) { + const statuses = policy?.lifecycle?.statuses || DEFAULT_ATLAS_POLICY.lifecycle.statuses; + return new Set(statuses.map((s) => s.key)); +} + +function assertLifecycleTransition(policy, org, toStatus, actor) { + const keys = statusKeys(policy); + if (!keys.has(toStatus)) { + const err = new Error(`Invalid lifecycle status: ${toStatus}`); + err.statusCode = 400; + throw err; + } + const fromStatus = org.lifecycleStatus || policy.lifecycle.defaultStatus || 'active'; + if (fromStatus === toStatus) { + return; + } + const transitions = policy?.lifecycle?.transitions || []; + const match = transitions.find((t) => t.from === fromStatus && t.to === toStatus); + if (!match) { + const err = new Error(`Transition from "${fromStatus}" to "${toStatus}" is not allowed`); + err.statusCode = 400; + throw err; + } + const allowed = match.allowedActors || ['admin', 'root']; + if (actor.isPlatformAdmin) { + return; + } + if (actor.isOfficer && allowed.includes('officer')) { + return; + } + const err = new Error('You are not allowed to perform this lifecycle transition'); + err.statusCode = 403; + throw err; +} + +function governanceRequirementsForOrg(policy, org) { + const key = org.orgTypeKey || policy.defaultOrgTypeKey || 'default'; + const orgType = (policy.orgTypes || []).find((t) => t.key === key); + if (orgType?.requiredGovernanceKeys?.length) { + return orgType.requiredGovernanceKeys; + } + const fallback = (policy.orgTypes || []).find((t) => t.key === policy.defaultOrgTypeKey); + return fallback?.requiredGovernanceKeys || ['constitution']; +} + +function shouldHideOrgFromPublicList(policy, org) { + if (!policy?.directory?.hideNonActiveFromPublicList) return false; + const nonActive = policy.directory.nonActiveStatuses || ['inactive']; + const status = org.lifecycleStatus || policy.lifecycle?.defaultStatus || 'active'; + return nonActive.includes(status); +} + +function assertOrgAllowsEventCreation(policy, org) { + if (!policy?.events?.inactiveOrgBlocksEventCreation) { + return { ok: true }; + } + const blocked = policy.events.blockedLifecycleStatuses || ['inactive', 'sunset']; + const status = org.lifecycleStatus || policy.lifecycle?.defaultStatus || 'active'; + if (blocked.includes(status)) { + return { + ok: false, + message: `This organization cannot create events while lifecycle status is "${status}".` + }; + } + return { ok: true }; +} + +function assertEventReservationReady(event, options = {}) { + const required = options.required !== false; + const resourceId = event?.reservation?.resourceId || event?.classroom_id || null; + if (!required || !resourceId) return { ok: true }; + const state = event?.reservation?.state || 'draft'; + const allowedStates = options.allowedStates || ['approved', 'requested', 'hold']; + if (!allowedStates.includes(state)) { + return { + ok: false, + message: `This event cannot proceed while reservation state is "${state}".`, + code: 'EVENT_RESERVATION_NOT_READY', + state + }; + } + if (event?.reservation?.conflictSummary?.hasConflict) { + const detectedAt = event?.reservation?.detectedAt ? new Date(event.reservation.detectedAt) : null; + const conflictAgeHours = detectedAt && !Number.isNaN(detectedAt.getTime()) + ? (Date.now() - detectedAt.getTime()) / (1000 * 60 * 60) + : 0; + const escalationThresholdHours = Number(options.escalationThresholdHours || process.env.RESERVATION_ESCALATION_THRESHOLD_HOURS || 24); + const escalated = conflictAgeHours >= escalationThresholdHours; + return { + ok: false, + message: event.reservation.conflictSummary.reason || 'This event has unresolved reservation conflicts.', + code: 'EVENT_RESERVATION_CONFLICT', + state, + escalated, + conflictAgeHours + }; + } + return { ok: true, state }; +} + +function getReservationEscalation(event, options = {}) { + if (!event?.reservation?.conflictSummary?.hasConflict) { + return { escalated: false, severity: 'none', ageHours: 0 }; + } + const detectedAt = event?.reservation?.detectedAt ? new Date(event.reservation.detectedAt) : null; + const ageHours = detectedAt && !Number.isNaN(detectedAt.getTime()) + ? (Date.now() - detectedAt.getTime()) / (1000 * 60 * 60) + : 0; + const threshold = Number(options.escalationThresholdHours || process.env.RESERVATION_ESCALATION_THRESHOLD_HOURS || 24); + const escalated = ageHours >= threshold; + const severity = escalated ? 'high' : 'medium'; + return { escalated, severity, ageHours, threshold }; +} + +function labelForGovernanceKey(policy, key) { + const t = policy?.terminology || {}; + return t[key] || key; +} + +module.exports = { + DEFAULT_ATLAS_POLICY, + deepMergeAtlasPolicy, + getEffectivePolicyFromConfig, + getEffectivePolicy, + assertLifecycleTransition, + governanceRequirementsForOrg, + shouldHideOrgFromPublicList, + assertOrgAllowsEventCreation, + assertEventReservationReady, + getReservationEscalation, + labelForGovernanceKey, + statusKeys +}; diff --git a/backend/services/budgetService.js b/backend/services/budgetService.js new file mode 100644 index 00000000..2c8094e5 --- /dev/null +++ b/backend/services/budgetService.js @@ -0,0 +1,768 @@ +const getModels = require('./getModelService'); +const { ORG_PERMISSIONS } = require('../constants/permissions'); +const { getEffectivePolicy } = require('./atlasPolicyService'); + +function defaultFinancePayload() { + return { + budgetTemplates: [ + { + templateKey: 'annual_club', + displayName: 'Annual club budget', + orgTypeKeys: ['default', 'club'], + fiscalLabel: 'Fiscal year', + workflowPresetKey: 'two_stage', + lineItemDefinitions: [ + { + key: 'operating', + label: 'Operating', + required: true, + kind: 'currency', + helpText: 'General operating funds requested' + }, + { + key: 'events', + label: 'Events & programs', + required: false, + kind: 'currency', + helpText: '' + }, + { + key: 'summary', + label: 'Summary notes', + required: false, + kind: 'text', + helpText: 'Optional context for reviewers' + } + ] + } + ], + workflowPresets: [ + { + presetKey: 'two_stage', + stages: [ + { + key: 'officer', + label: 'Officer review', + actorType: 'org_permission', + permission: ORG_PERMISSIONS.MANAGE_FINANCES + }, + { + key: 'finance_office', + label: 'Finance office', + actorType: 'platform_admin', + permission: '' + } + ] + } + ] + }; +} + +async function ensureFinanceConfig(req) { + const { FinanceConfig } = getModels(req, 'FinanceConfig'); + let doc = await FinanceConfig.findOne(); + if (!doc) { + doc = new FinanceConfig(defaultFinancePayload()); + await doc.save(); + } else if ( + (!doc.budgetTemplates || doc.budgetTemplates.length === 0) && + (!doc.workflowPresets || doc.workflowPresets.length === 0) + ) { + const defs = defaultFinancePayload(); + doc.budgetTemplates = defs.budgetTemplates; + doc.workflowPresets = defs.workflowPresets; + await doc.save(); + } + return doc; +} + +function getPreset(config, presetKey) { + const key = presetKey || 'two_stage'; + return (config.workflowPresets || []).find((p) => p.presetKey === key) || config.workflowPresets?.[0]; +} + +/** Append workflow audit entry (mongoose document). */ +function pushAudit(budget, { userId, actor, action, message = '', fromStatus = '', toStatus = '', stageKey = '' }) { + if (!budget.auditLog) budget.auditLog = []; + budget.auditLog.push({ + at: new Date(), + userId, + actor, + action, + message: message || '', + fromStatus: fromStatus || '', + toStatus: toStatus || '', + stageKey: stageKey || '' + }); +} + +function pickTemplateForOrg(config, org) { + const typeKey = org.orgTypeKey || 'default'; + const templates = config.budgetTemplates || []; + const match = templates.find( + (t) => !t.orgTypeKeys?.length || t.orgTypeKeys.includes(typeKey) || t.orgTypeKeys.includes('default') + ); + return match || templates[0]; +} + +function normalizeCustomLineItemKey(raw, fallback) { + const base = String(raw || fallback || '') + .trim() + .toLowerCase() + .replace(/[^a-z0-9_]+/g, '_') + .replace(/^_+|_+$/g, ''); + return base || ''; +} + +function materializeLineItems(template, incoming, options = {}) { + const allowCustomLineItems = options.allowCustomLineItems === true; + const maxCustomLineItems = Math.min(100, Math.max(0, Number(options.maxCustomLineItems) || 20)); + const defs = template?.lineItemDefinitions || []; + const byKey = {}; + (incoming || []).forEach((row) => { + if (row && row.key) byKey[row.key] = row; + }); + const baseItems = defs.map((def) => { + const row = byKey[def.key] || {}; + const base = { + key: def.key, + label: def.label, + kind: def.kind || 'currency', + amount: null, + numberValue: null, + textValue: '', + note: row.note != null ? String(row.note) : '' + }; + if (def.kind === 'currency') { + base.amount = row.amount != null && row.amount !== '' ? Number(row.amount) : null; + } else if (def.kind === 'number') { + base.numberValue = row.numberValue != null && row.numberValue !== '' ? Number(row.numberValue) : null; + } else { + base.textValue = row.textValue != null ? String(row.textValue) : ''; + } + return base; + }); + + if (!allowCustomLineItems) return baseItems; + + const templateKeys = new Set(defs.map((d) => String(d.key))); + const usedKeys = new Set(baseItems.map((li) => String(li.key))); + const customItems = []; + + for (const row of incoming || []) { + if (!row || row.key == null) continue; + const originalKey = String(row.key); + if (!originalKey || templateKeys.has(originalKey)) continue; + + const normalizedKey = normalizeCustomLineItemKey(originalKey, row.label || 'custom'); + if (!normalizedKey || usedKeys.has(normalizedKey)) continue; + usedKeys.add(normalizedKey); + + const kind = ['currency', 'number', 'text'].includes(row.kind) ? row.kind : 'currency'; + const item = { + key: normalizedKey, + label: row.label != null && String(row.label).trim() ? String(row.label).trim() : originalKey, + kind, + amount: null, + numberValue: null, + textValue: '', + note: row.note != null ? String(row.note) : '' + }; + + if (kind === 'currency') { + item.amount = row.amount != null && row.amount !== '' ? Number(row.amount) : null; + } else if (kind === 'number') { + item.numberValue = row.numberValue != null && row.numberValue !== '' ? Number(row.numberValue) : null; + } else { + item.textValue = row.textValue != null ? String(row.textValue) : ''; + } + + customItems.push(item); + if (customItems.length >= maxCustomLineItems) break; + } + + return [...baseItems, ...customItems]; +} + +function validateRequiredLineItems(template, lineItems) { + const defs = template?.lineItemDefinitions || []; + for (const def of defs) { + if (!def.required) continue; + const li = lineItems.find((x) => x.key === def.key); + if (!li) return { ok: false, message: `Missing line item: ${def.label || def.key}` }; + if (def.kind === 'currency' && (li.amount == null || Number.isNaN(li.amount))) { + return { ok: false, message: `Required amount: ${def.label || def.key}` }; + } + if (def.kind === 'number' && (li.numberValue == null || Number.isNaN(li.numberValue))) { + return { ok: false, message: `Required number: ${def.label || def.key}` }; + } + if (def.kind === 'text' && (!li.textValue || !String(li.textValue).trim())) { + return { ok: false, message: `Required text: ${def.label || def.key}` }; + } + } + return { ok: true }; +} + +function nextRevisionNumber(budget) { + const revs = budget.revisions || []; + if (!revs.length) return 1; + return Math.max(...revs.map((r) => r.revision || 0)) + 1; +} + +async function listBudgetsForOrg(req, orgId) { + const { OrgBudget } = getModels(req, 'OrgBudget'); + return OrgBudget.find({ orgId }).sort({ updatedAt: -1 }).lean(); +} + +async function getBudgetById(req, orgId, budgetId) { + const { OrgBudget } = getModels(req, 'OrgBudget'); + const b = await OrgBudget.findOne({ _id: budgetId, orgId }).lean(); + return b; +} + +async function getBudgetLineItemPolicy(req) { + const atlasPolicy = await getEffectivePolicy(req); + const budgetPolicy = atlasPolicy?.budgets || {}; + const lineItemMode = budgetPolicy.lineItemMode === 'template_plus_custom' + ? 'template_plus_custom' + : 'template_only'; + const maxCustomLineItems = Math.min( + 100, + Math.max(0, Number(budgetPolicy.maxCustomLineItems) || 20) + ); + return { + lineItemMode, + allowCustomLineItems: lineItemMode === 'template_plus_custom', + maxCustomLineItems + }; +} + +async function createBudget(req, orgId, userId, { templateKey, fiscalYear, title }) { + const { Org, OrgBudget } = getModels(req, 'Org', 'OrgBudget'); + const org = await Org.findById(orgId); + if (!org) { + const err = new Error('Organization not found'); + err.statusCode = 404; + throw err; + } + const config = await ensureFinanceConfig(req); + const template = + (config.budgetTemplates || []).find((t) => t.templateKey === templateKey) || pickTemplateForOrg(config, org); + if (!template) { + const err = new Error('No budget template configured'); + err.statusCode = 400; + throw err; + } + const fy = fiscalYear != null ? String(fiscalYear) : String(new Date().getFullYear()); + const dup = await OrgBudget.findOne({ + orgId, + fiscalYear: fy, + templateKey: template.templateKey, + status: { $nin: ['rejected'] } + }) + .select('_id') + .lean(); + if (dup) { + const err = new Error( + 'A budget already exists for this fiscal year and template. Continue the existing one or reject it before starting another.' + ); + err.statusCode = 409; + throw err; + } + const lineItems = materializeLineItems(template, []); + const budget = new OrgBudget({ + orgId, + templateKey: template.templateKey, + fiscalYear: fy, + title: title || `${template.displayName} ${fy}`, + status: 'draft', + lineItems, + workflow: { + presetKey: template.workflowPresetKey || 'two_stage', + currentStageIndex: 0, + stagesSnapshot: [], + completedStages: [] + }, + createdBy: userId, + updatedBy: userId + }); + pushAudit(budget, { + userId, + actor: 'org', + action: 'draft_created', + toStatus: 'draft', + message: title ? `Title: ${title}` : '' + }); + await budget.save(); + return budget.toObject(); +} + +async function updateBudgetDraft(req, orgId, budgetId, userId, { lineItems: incoming, title }) { + const { OrgBudget } = getModels(req, 'OrgBudget'); + const budget = await OrgBudget.findOne({ _id: budgetId, orgId }); + if (!budget) { + const err = new Error('Budget not found'); + err.statusCode = 404; + throw err; + } + if (!['draft', 'revision_requested'].includes(budget.status)) { + const err = new Error('Budget cannot be edited in its current status'); + err.statusCode = 400; + throw err; + } + const config = await ensureFinanceConfig(req); + const template = (config.budgetTemplates || []).find((t) => t.templateKey === budget.templateKey); + if (!template) { + const err = new Error('Template missing'); + err.statusCode = 400; + throw err; + } + if (title != null) budget.title = String(title); + const lineItemPolicy = await getBudgetLineItemPolicy(req); + budget.lineItems = materializeLineItems(template, incoming, lineItemPolicy); + budget.updatedBy = userId; + if (budget.status === 'revision_requested') { + pushAudit(budget, { + userId, + actor: 'org', + action: 'resumed_after_revision', + fromStatus: 'revision_requested', + toStatus: 'draft' + }); + budget.status = 'draft'; + } + await budget.save(); + return budget.toObject(); +} + +async function addComment(req, orgId, budgetId, userId, { body }) { + const { OrgBudget } = getModels(req, 'OrgBudget'); + const budget = await OrgBudget.findOne({ _id: budgetId, orgId }); + if (!budget) { + const err = new Error('Budget not found'); + err.statusCode = 404; + throw err; + } + const text = (body || '').trim(); + if (!text) { + const err = new Error('Comment body required'); + err.statusCode = 400; + throw err; + } + const revision = + budget.revisions && budget.revisions.length ? budget.revisions[budget.revisions.length - 1].revision : null; + budget.comments.push({ userId, body: text, revision }); + budget.workflow.lastActionAt = new Date(); + budget.workflow.lastActionBy = userId; + await budget.save(); + return budget.toObject(); +} + +async function submitBudget(req, orgId, budgetId, userId) { + const { OrgBudget } = getModels(req, 'OrgBudget'); + const budget = await OrgBudget.findOne({ _id: budgetId, orgId }); + if (!budget) { + const err = new Error('Budget not found'); + err.statusCode = 404; + throw err; + } + if (!['draft', 'revision_requested'].includes(budget.status)) { + const err = new Error('Only draft or revision-requested budgets can be submitted'); + err.statusCode = 400; + throw err; + } + const config = await ensureFinanceConfig(req); + const template = (config.budgetTemplates || []).find((t) => t.templateKey === budget.templateKey); + const v = validateRequiredLineItems(template, budget.lineItems || []); + if (!v.ok) { + const err = new Error(v.message); + err.statusCode = 400; + throw err; + } + const preset = getPreset(config, budget.workflow?.presetKey || template?.workflowPresetKey); + if (!preset || !preset.stages?.length) { + const err = new Error('Workflow preset not configured'); + err.statusCode = 500; + throw err; + } + const rev = nextRevisionNumber(budget); + budget.revisions.push({ + revision: rev, + createdBy: userId, + lineItemsSnapshot: JSON.parse(JSON.stringify(budget.lineItems || [])), + workflowSnapshot: JSON.parse(JSON.stringify(budget.workflow || {})), + status: budget.status + }); + budget.workflow.presetKey = preset.presetKey; + budget.workflow.stagesSnapshot = preset.stages.map((s) => ({ + key: s.key, + label: s.label, + actorType: s.actorType, + permission: s.permission || '' + })); + budget.workflow.currentStageIndex = 0; + budget.workflow.completedStages = []; + budget.workflow.lastActionAt = new Date(); + budget.workflow.lastActionBy = userId; + const prevStatus = budget.status; + budget.status = 'in_review'; + budget.updatedBy = userId; + pushAudit(budget, { + userId, + actor: 'org', + action: 'submitted', + fromStatus: prevStatus, + toStatus: 'in_review' + }); + await budget.save(); + return budget.toObject(); +} + +function currentStage(budget) { + const stages = budget.workflow?.stagesSnapshot || []; + const idx = budget.workflow?.currentStageIndex ?? 0; + return stages[idx] || null; +} + +async function approveStageOrg(req, orgId, budgetId, userId, stageKey) { + const { OrgBudget } = getModels(req, 'OrgBudget'); + const budget = await OrgBudget.findOne({ _id: budgetId, orgId }); + if (!budget) { + const err = new Error('Budget not found'); + err.statusCode = 404; + throw err; + } + if (budget.status !== 'in_review') { + const err = new Error('Budget is not in review'); + err.statusCode = 400; + throw err; + } + const stage = currentStage(budget); + if (!stage || stage.key !== stageKey) { + const err = new Error('Invalid workflow stage'); + err.statusCode = 400; + throw err; + } + if (stage.actorType !== 'org_permission') { + const err = new Error('This stage is not an organization approval step'); + err.statusCode = 403; + throw err; + } + const beforeStatus = budget.status; + budget.workflow.completedStages.push({ key: stage.key, approvedBy: userId, approvedAt: new Date() }); + budget.workflow.currentStageIndex = (budget.workflow.currentStageIndex || 0) + 1; + budget.workflow.lastActionAt = new Date(); + budget.workflow.lastActionBy = userId; + const stages = budget.workflow.stagesSnapshot || []; + if (budget.workflow.currentStageIndex >= stages.length) { + budget.status = 'approved'; + } + budget.updatedBy = userId; + pushAudit(budget, { + userId, + actor: 'org', + action: budget.status === 'approved' ? 'approved' : 'officer_stage_approved', + fromStatus: beforeStatus, + toStatus: budget.status, + stageKey: stage.key + }); + await budget.save(); + return budget.toObject(); +} + +async function approveStagePlatform(req, orgId, budgetId, userId, stageKey) { + const { OrgBudget } = getModels(req, 'OrgBudget'); + const budget = await OrgBudget.findOne({ _id: budgetId, orgId }); + if (!budget) { + const err = new Error('Budget not found'); + err.statusCode = 404; + throw err; + } + if (budget.status !== 'in_review') { + const err = new Error('Budget is not in review'); + err.statusCode = 400; + throw err; + } + const stage = currentStage(budget); + if (!stage || stage.key !== stageKey) { + const err = new Error('Invalid workflow stage'); + err.statusCode = 400; + throw err; + } + if (stage.actorType !== 'platform_admin') { + const err = new Error('This stage is not a platform admin step'); + err.statusCode = 400; + throw err; + } + const beforeStatus = budget.status; + budget.workflow.completedStages.push({ key: stage.key, approvedBy: userId, approvedAt: new Date() }); + budget.workflow.currentStageIndex = (budget.workflow.currentStageIndex || 0) + 1; + budget.workflow.lastActionAt = new Date(); + budget.workflow.lastActionBy = userId; + const stages = budget.workflow.stagesSnapshot || []; + if (budget.workflow.currentStageIndex >= stages.length) { + budget.status = 'approved'; + } + budget.updatedBy = userId; + pushAudit(budget, { + userId, + actor: 'platform', + action: budget.status === 'approved' ? 'approved' : 'platform_stage_approved', + fromStatus: beforeStatus, + toStatus: budget.status, + stageKey: stage.key + }); + await budget.save(); + return budget.toObject(); +} + +async function rejectBudget(req, orgId, budgetId, userId, { message, stageKey }, { platformOnly }) { + const { OrgBudget } = getModels(req, 'OrgBudget'); + const budget = await OrgBudget.findOne({ _id: budgetId, orgId }); + if (!budget) { + const err = new Error('Budget not found'); + err.statusCode = 404; + throw err; + } + if (budget.status !== 'in_review') { + const err = new Error('Budget is not in review'); + err.statusCode = 400; + throw err; + } + const stage = currentStage(budget); + if (!stage) { + const err = new Error('Invalid workflow'); + err.statusCode = 400; + throw err; + } + if (stageKey && stage.key !== stageKey) { + const err = new Error('Invalid workflow stage'); + err.statusCode = 400; + throw err; + } + if (platformOnly && stage.actorType !== 'platform_admin') { + const err = new Error('Use the organization route for this stage'); + err.statusCode = 400; + throw err; + } + if (!platformOnly && stage.actorType !== 'org_permission') { + const err = new Error('Use the admin route for this stage'); + err.statusCode = 400; + throw err; + } + const beforeStatus = budget.status; + budget.status = 'rejected'; + budget.workflow.lastActionAt = new Date(); + budget.workflow.lastActionBy = userId; + budget.updatedBy = userId; + const msg = (message && String(message).trim()) || ''; + if (msg) { + budget.comments.push({ userId, body: `Rejected: ${msg}`, revision: null }); + } + pushAudit(budget, { + userId, + actor: platformOnly ? 'platform' : 'org', + action: 'rejected', + fromStatus: beforeStatus, + toStatus: 'rejected', + stageKey: stage.key, + message: msg + }); + await budget.save(); + return budget.toObject(); +} + +async function requestRevision(req, orgId, budgetId, userId, { message, stageKey }, { platformOnly }) { + const { OrgBudget } = getModels(req, 'OrgBudget'); + const budget = await OrgBudget.findOne({ _id: budgetId, orgId }); + if (!budget) { + const err = new Error('Budget not found'); + err.statusCode = 404; + throw err; + } + if (budget.status !== 'in_review') { + const err = new Error('Budget is not in review'); + err.statusCode = 400; + throw err; + } + const stage = currentStage(budget); + if (!stage) { + const err = new Error('Invalid workflow'); + err.statusCode = 400; + throw err; + } + if (stageKey && stage.key !== stageKey) { + const err = new Error('Invalid workflow stage'); + err.statusCode = 400; + throw err; + } + if (platformOnly && stage.actorType !== 'platform_admin') { + const err = new Error('Use the organization route for this stage'); + err.statusCode = 400; + throw err; + } + if (!platformOnly && stage.actorType !== 'org_permission') { + const err = new Error('Use the admin route for this stage'); + err.statusCode = 400; + throw err; + } + const trimmed = String(message || '').trim(); + if (!trimmed) { + const err = new Error('A note explaining the requested changes is required.'); + err.statusCode = 400; + throw err; + } + const beforeStatus = budget.status; + const rev = nextRevisionNumber(budget); + budget.revisions.push({ + revision: rev, + createdBy: userId, + lineItemsSnapshot: JSON.parse(JSON.stringify(budget.lineItems || [])), + workflowSnapshot: JSON.parse(JSON.stringify(budget.workflow || {})), + status: 'revision_requested' + }); + budget.status = 'revision_requested'; + budget.workflow.stagesSnapshot = []; + budget.workflow.currentStageIndex = 0; + budget.workflow.completedStages = []; + budget.workflow.lastActionAt = new Date(); + budget.workflow.lastActionBy = userId; + budget.updatedBy = userId; + budget.comments.push({ userId, body: `Revision requested: ${trimmed}`, revision: rev }); + pushAudit(budget, { + userId, + actor: platformOnly ? 'platform' : 'org', + action: 'revision_requested', + fromStatus: beforeStatus, + toStatus: 'revision_requested', + stageKey: stage.key, + message: trimmed + }); + await budget.save(); + return budget.toObject(); +} + +async function listBudgetsAdmin(req, { status, search, page = 1, limit = 30 }) { + const { OrgBudget, Org } = getModels(req, 'OrgBudget', 'Org'); + const filter = {}; + if (status) filter.status = status; + const skip = (Math.max(1, parseInt(page, 10)) - 1) * Math.min(100, Math.max(1, parseInt(limit, 10))); + const lim = Math.min(100, Math.max(1, parseInt(limit, 10))); + let orgIds = null; + if (search && String(search).trim()) { + const rx = new RegExp(String(search).trim(), 'i'); + const orgs = await Org.find({ org_name: rx }).select('_id').lean(); + orgIds = orgs.map((o) => o._id); + if (!orgIds.length) { + return { data: [], total: 0, page: parseInt(page, 10), limit: lim }; + } + filter.orgId = { $in: orgIds }; + } + const total = await OrgBudget.countDocuments(filter); + const rows = await OrgBudget.find(filter).sort({ updatedAt: -1 }).skip(skip).limit(lim).lean(); + const ids = [...new Set(rows.map((r) => String(r.orgId)))]; + const orgDocs = await Org.find({ _id: { $in: ids } }) + .select('org_name org_profile_image orgTypeKey') + .lean(); + const orgMap = Object.fromEntries(orgDocs.map((o) => [String(o._id), o])); + const enriched = rows.map((r) => ({ + ...r, + org: orgMap[String(r.orgId)] || null + })); + return { data: enriched, total, page: parseInt(page, 10), limit: lim }; +} + +async function getFinanceConfigDoc(req) { + return ensureFinanceConfig(req); +} + +async function updateFinanceConfig(req, patch) { + const { FinanceConfig } = getModels(req, 'FinanceConfig'); + await ensureFinanceConfig(req); + const doc = await FinanceConfig.findOne(); + if (patch.budgetTemplates) doc.budgetTemplates = patch.budgetTemplates; + if (patch.workflowPresets) doc.workflowPresets = patch.workflowPresets; + await doc.save(); + return doc.toObject(); +} + +function budgetToExportRows(budget) { + const rows = []; + rows.push(['field', 'col2', 'col3', 'col4', 'col5', 'col6']); + rows.push(['budgetId', String(budget._id), '', '', '', '']); + rows.push(['title', budget.title || '', '', '', '', '']); + rows.push(['fiscalYear', budget.fiscalYear || '', '', '', '', '']); + rows.push(['templateKey', budget.templateKey || '', '', '', '', '']); + rows.push(['status', budget.status || '', '', '', '', '']); + rows.push(['---', 'lineItems', '', '', '', '']); + rows.push(['key', 'label', 'amount', 'numberValue', 'textValue', 'note']); + for (const li of budget.lineItems || []) { + rows.push([ + li.key, + li.label || '', + li.amount != null ? String(li.amount) : '', + li.numberValue != null ? String(li.numberValue) : '', + li.textValue || '', + li.note || '' + ]); + } + return rows; +} + +function toCsv(rows) { + return rows + .map((r) => + r + .map((cell) => { + const s = cell == null ? '' : String(cell); + if (/[",\n]/.test(s)) return `"${s.replace(/"/g, '""')}"`; + return s; + }) + .join(',') + ) + .join('\n'); +} + +async function exportBudget(req, orgId, budgetId, format) { + const budget = await getBudgetById(req, orgId, budgetId); + if (!budget) { + const err = new Error('Budget not found'); + err.statusCode = 404; + throw err; + } + if (format === 'csv') { + const rows = budgetToExportRows(budget); + return { contentType: 'text/csv; charset=utf-8', body: toCsv(rows), filename: `budget-${budgetId}.csv` }; + } + return { + contentType: 'application/json', + body: { success: true, data: budget }, + filename: null + }; +} + +module.exports = { + ensureFinanceConfig, + defaultFinancePayload, + pickTemplateForOrg, + getPreset, + getBudgetLineItemPolicy, + listBudgetsForOrg, + getBudgetById, + createBudget, + updateBudgetDraft, + addComment, + submitBudget, + approveStageOrg, + approveStagePlatform, + rejectBudget, + requestRevision, + listBudgetsAdmin, + getFinanceConfigDoc, + updateFinanceConfig, + exportBudget, + materializeLineItems, + validateRequiredLineItems, + budgetToExportRows, + toCsv +}; diff --git a/backend/services/externalRoomSyncService.js b/backend/services/externalRoomSyncService.js new file mode 100644 index 00000000..4170c6ee --- /dev/null +++ b/backend/services/externalRoomSyncService.js @@ -0,0 +1,25 @@ +class ExternalRoomSyncService { + constructor(req) { + this.req = req; + } + + async dryRunSyncReservation(event, options = {}) { + const provider = options.provider || process.env.RESERVATION_SYNC_PROVIDER || 'none'; + const enabled = process.env.RESERVATION_SYNC_DRY_RUN === 'true'; + if (!enabled) { + return { enabled: false, status: 'skipped', provider, reason: 'Dry-run sync disabled' }; + } + + const hasConflict = Boolean(event?.reservation?.conflictSummary?.hasConflict); + return { + enabled: true, + status: hasConflict ? 'conflict' : 'ok', + provider, + externalResourceId: String(event?.reservation?.resourceId || event?.classroom_id || ''), + checkedAt: new Date(), + recommendation: hasConflict ? 'manual_review' : 'safe_to_sync' + }; + } +} + +module.exports = ExternalRoomSyncService; diff --git a/backend/services/getModelService.js b/backend/services/getModelService.js index 7befaf12..c2d7b2a3 100644 --- a/backend/services/getModelService.js +++ b/backend/services/getModelService.js @@ -8,6 +8,7 @@ const OIEConfigSchema = require('../schemas/OIEConfig'); const orgSchema = require('../schemas/org'); const orgFollowerSchema = require('../schemas/orgFollower'); const orgMemberSchema = require('../schemas/orgMember'); +const orgMembershipAuditSchema = require('../schemas/orgMembershipAudit'); const orgInviteSchema = require('../schemas/orgInvite'); const eventCollaborationInviteSchema = require('../schemas/eventCollaborationInvite'); const qrSchema = require('../schemas/qr'); @@ -17,6 +18,8 @@ const reportSchema = require('../schemas/report'); const scheduleSchema = require('../schemas/schedule'); const searchSchema = require('../schemas/search'); const studyHistorySchema = require('../schemas/studyHistory'); +const taskSchema = require('../schemas/task'); +const eventTemplateSchema = require('../schemas/eventTemplate'); const userSchema = require('../schemas/user'); const visitSchema = require('../schemas/visit'); const sessionSchema = require('../schemas/session'); @@ -47,6 +50,8 @@ const formSchema = require('../events/schemas/form'); const formResponseSchema = require('../events/schemas/formResponse'); const orgVerificationSchema = require('../schemas/orgVerification'); const orgManagementConfigSchema = require('../schemas/orgManagementConfig'); +const financeConfigSchema = require('../schemas/financeConfig'); +const orgBudgetSchema = require('../schemas/orgBudget'); const eventAnalyticsSchema = require('../events/schemas/eventAnalytics'); const eventSystemConfigSchema = require('../events/schemas/eventSystemConfig'); const stakeholderRoleSchema = require('../events/schemas/stakeholderRole'); @@ -64,6 +69,7 @@ const MODEL_DEFINITIONS = Object.freeze({ BadgeGrant: { modelName: 'BadgeGrant', schema: badgeGrantSchema, collection: 'badgegrants' }, Building: { modelName: 'Building', schema: buildingSchema, collection: 'buildings' }, Classroom: { modelName: 'Classroom', schema: classroomSchema, collection: 'classrooms1' }, + Room: { modelName: 'Classroom', schema: classroomSchema, collection: 'classrooms1' }, Developer: { modelName: 'Developer', schema: developerSchema, collection: 'developers' }, Event: { modelName: 'Event', schema: eventSchema, collection: 'events' }, Friendship: { modelName: 'Friendship', schema: friendshipSchema, collection: 'friendships' }, @@ -72,6 +78,7 @@ const MODEL_DEFINITIONS = Object.freeze({ Org: { modelName: 'Org', schema: orgSchema, collection: 'orgs' }, OrgFollower: { modelName: 'OrgFollower', schema: orgFollowerSchema, collection: 'followers' }, OrgMember: { modelName: 'OrgMember', schema: orgMemberSchema, collection: 'members' }, + OrgMembershipAudit: { modelName: 'OrgMembershipAudit', schema: orgMembershipAuditSchema, collection: 'orgMembershipAudits' }, OrgInvite: { modelName: 'OrgInvite', schema: orgInviteSchema, collection: 'orgInvites' }, EventCollaborationInvite: { modelName: 'EventCollaborationInvite', schema: eventCollaborationInviteSchema, collection: 'eventCollaborationInvites' }, QR: { modelName: 'QR', schema: qrSchema, collection: 'QR' }, @@ -81,6 +88,8 @@ const MODEL_DEFINITIONS = Object.freeze({ Schedule: { modelName: 'Schedule', schema: scheduleSchema, collection: 'schedules' }, Search: { modelName: 'Search', schema: searchSchema, collection: 'searches' }, StudyHistory: { modelName: 'StudyHistory', schema: studyHistorySchema, collection: 'studyHistories' }, + History: { modelName: 'StudyHistory', schema: studyHistorySchema, collection: 'studyHistories' }, + Task: { modelName: 'Task', schema: taskSchema, collection: 'tasks' }, User: { modelName: 'User', schema: userSchema, collection: 'users' }, Visit: { modelName: 'Visit', schema: visitSchema, collection: 'visits' }, Session: { modelName: 'Session', schema: sessionSchema, collection: 'sessions' }, @@ -89,9 +98,12 @@ const MODEL_DEFINITIONS = Object.freeze({ RssFeed: { modelName: 'RssFeed', schema: rssFeedSchema, collection: 'rssFeeds' }, Form: { modelName: 'Form', schema: formSchema, collection: 'forms' }, FormResponse: { modelName: 'FormResponse', schema: formResponseSchema, collection: 'formResponses' }, + EventTemplate: { modelName: 'EventTemplate', schema: eventTemplateSchema, collection: 'eventtemplates' }, StudySession: { modelName: 'StudySession', schema: studySessionSchema, collection: 'studySessions' }, OrgVerification: { modelName: 'OrgVerification', schema: orgVerificationSchema, collection: 'orgVerifications' }, OrgManagementConfig: { modelName: 'OrgManagementConfig', schema: orgManagementConfigSchema, collection: 'orgManagementConfigs' }, + FinanceConfig: { modelName: 'FinanceConfig', schema: financeConfigSchema, collection: 'financeConfigs' }, + OrgBudget: { modelName: 'OrgBudget', schema: orgBudgetSchema, collection: 'orgBudgets' }, OrgMemberApplication: { modelName: 'OrgMemberApplication', schema: orgMemberApplicationSchema, collection: 'orgMemberApplications' }, SAMLConfig: { modelName: 'SAMLConfig', schema: samlConfigSchema, collection: 'samlConfigs' }, Notification: { modelName: 'Notification', schema: notificationSchema, collection: 'notifications' }, diff --git a/backend/services/imageUploadService.js b/backend/services/imageUploadService.js index aebd2776..d9b04b0c 100644 --- a/backend/services/imageUploadService.js +++ b/backend/services/imageUploadService.js @@ -123,8 +123,50 @@ const deleteAndUploadImageToS3 = async (file, folderName, existingImageUrl, cust } }; -module.exports = { - uploadImageToS3, +const GOVERNANCE_DOC_MIME_TYPES = ['application/pdf']; +const MAX_GOVERNANCE_DOC_SIZE = 15 * 1024 * 1024; + +const governanceDocFileFilter = (req, file, cb) => { + if (!GOVERNANCE_DOC_MIME_TYPES.includes(file.mimetype)) { + cb(new Error('Only PDF files are allowed for governance documents.'), false); + return; + } + cb(null, true); +}; + +const governanceUpload = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: MAX_GOVERNANCE_DOC_SIZE }, + fileFilter: governanceDocFileFilter +}); + +const uploadDocumentToS3 = async (file, folderName, customFileName = null) => { + if (!file) { + throw new Error('No file provided.'); + } + if (!GOVERNANCE_DOC_MIME_TYPES.includes(file.mimetype)) { + throw new Error('Invalid file type for document upload.'); + } + if (file.size > MAX_GOVERNANCE_DOC_SIZE) { + throw new Error('File size exceeds limit.'); + } + const fileName = customFileName || generateSecureFileName(file.originalname, 'gov-'); + const s3Params = { + Bucket: process.env.AWS_S3_BUCKET_NAME, + Key: `${folderName}/${fileName}`, + Body: file.buffer, + ContentType: file.mimetype, + ContentDisposition: 'inline', + CacheControl: 'public, max-age=31536000' + }; + const s3Response = await s3.upload(s3Params).promise(); + return s3Response.Location; +}; + +module.exports = { + uploadImageToS3, deleteAndUploadImageToS3, - upload // Export the multer upload middleware + upload, + uploadDocumentToS3, + governanceUpload }; diff --git a/backend/services/orgMembershipService.js b/backend/services/orgMembershipService.js new file mode 100644 index 00000000..d3d4a9b1 --- /dev/null +++ b/backend/services/orgMembershipService.js @@ -0,0 +1,56 @@ +const getModels = require('./getModelService'); + +/** + * @param {import('mongoose').Document} member + * @param {object} partial + */ +function pushMemberHistory(member, partial) { + if (!member.membershipHistory) { + member.membershipHistory = []; + } + member.membershipHistory.push({ + at: new Date(), + ...partial + }); +} + +async function recordMemberJoined(member, actorUserId, reason = '') { + pushMemberHistory(member, { + action: 'joined', + toStatus: member.status, + role: member.role, + actorUserId, + reason + }); + await member.save(); +} + +async function recordMemberRemoved(req, { org_id, user_id, actorUserId, reason = '' }) { + const { OrgMembershipAudit } = getModels(req, 'OrgMembershipAudit'); + await OrgMembershipAudit.create({ + org_id, + user_id, + action: 'removed', + actorUserId, + reason, + at: new Date() + }); +} + +async function recordStatusChange(member, { fromStatus, toStatus, actorUserId, reason = '' }) { + pushMemberHistory(member, { + action: 'status_change', + fromStatus, + toStatus, + actorUserId, + reason + }); + await member.save(); +} + +module.exports = { + pushMemberHistory, + recordMemberJoined, + recordMemberRemoved, + recordStatusChange +}; diff --git a/backend/services/reservationMetricsService.js b/backend/services/reservationMetricsService.js new file mode 100644 index 00000000..63f027a5 --- /dev/null +++ b/backend/services/reservationMetricsService.js @@ -0,0 +1,93 @@ +const getModels = require('./getModelService'); + +class ReservationMetricsService { + constructor(req) { + this.req = req; + this.models = getModels(req, 'Event'); + } + + static toDateSafe(value, fallback) { + const d = value ? new Date(value) : null; + if (!d || Number.isNaN(d.getTime())) return fallback; + return d; + } + + async getMetrics({ orgId = null, startDate, endDate } = {}) { + const { Event } = this.models; + const now = new Date(); + const start = ReservationMetricsService.toDateSafe(startDate, new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)); + const end = ReservationMetricsService.toDateSafe(endDate, now); + + const match = { + isDeleted: false, + start_time: { $gte: start, $lte: end }, + 'reservation.resourceId': { $ne: null } + }; + if (orgId) { + match.$or = [{ hostingId: orgId }, { 'collaboratorOrgs.orgId': orgId }]; + } + + const [summary] = await Event.aggregate([ + { $match: match }, + { + $group: { + _id: null, + totalReservations: { $sum: 1 }, + conflicts: { $sum: { $cond: ['$reservation.conflictSummary.hasConflict', 1, 0] } }, + unresolved: { + $sum: { + $cond: [ + { + $and: [ + '$reservation.conflictSummary.hasConflict', + { $ne: ['$reservation.resolutionStatus', 'resolved'] } + ] + }, + 1, + 0 + ] + } + }, + approvedReservations: { $sum: { $cond: [{ $eq: ['$reservation.state', 'approved'] }, 1, 0] } } + } + } + ]); + + const byResource = await Event.aggregate([ + { $match: match }, + { + $group: { + _id: '$reservation.resourceId', + reservations: { $sum: 1 }, + conflicts: { $sum: { $cond: ['$reservation.conflictSummary.hasConflict', 1, 0] } } + } + }, + { $sort: { reservations: -1 } }, + { $limit: 100 } + ]); + + return { + window: { start, end }, + totalReservations: summary?.totalReservations || 0, + conflicts: summary?.conflicts || 0, + unresolved: summary?.unresolved || 0, + approvedReservations: summary?.approvedReservations || 0, + conflictRate: summary?.totalReservations ? (summary.conflicts / summary.totalReservations) : 0, + byResource + }; + } + + static toCsv(metrics = {}) { + const rows = [ + ['metric', 'value'], + ['totalReservations', metrics.totalReservations || 0], + ['conflicts', metrics.conflicts || 0], + ['unresolved', metrics.unresolved || 0], + ['approvedReservations', metrics.approvedReservations || 0], + ['conflictRate', metrics.conflictRate || 0] + ]; + return rows.map((row) => row.join(',')).join('\n'); + } +} + +module.exports = ReservationMetricsService; diff --git a/backend/services/resourceReservationService.js b/backend/services/resourceReservationService.js new file mode 100644 index 00000000..fd3b16d5 --- /dev/null +++ b/backend/services/resourceReservationService.js @@ -0,0 +1,205 @@ +const getModels = require('./getModelService'); + +const ACTIVE_EVENT_STATUSES = ['approved', 'not-applicable', 'pending', 'draft']; + +class ResourceReservationService { + constructor(req) { + this.req = req; + this.models = getModels(req, 'Event', 'Classroom', 'Schedule'); + } + + static parseDate(value) { + const parsed = value instanceof Date ? value : new Date(value); + if (Number.isNaN(parsed.getTime())) return null; + return parsed; + } + + static inferReservationState(eventStatus) { + if (eventStatus === 'approved' || eventStatus === 'not-applicable') return 'approved'; + if (eventStatus === 'rejected') return 'rejected'; + if (eventStatus === 'pending') return 'requested'; + return 'draft'; + } + + normalizeEventReservation(eventLike = {}) { + const current = eventLike.reservation || {}; + const resourceId = current.resourceId || eventLike.classroom_id || null; + const state = current.state || ResourceReservationService.inferReservationState(eventLike.status); + return { + resourceId, + state, + lastCheckedAt: current.lastCheckedAt || null, + conflictSummary: current.conflictSummary || { hasConflict: false, reason: '' }, + conflictType: current.conflictType || '', + conflictSource: current.conflictSource || '', + detectedAt: current.detectedAt || null, + resolutionStatus: current.resolutionStatus || 'resolved', + resolutionNote: current.resolutionNote || '', + assignedTo: current.assignedTo || null, + history: Array.isArray(current.history) ? current.history : [], + sync: { + sourceOfTruth: current.sync?.sourceOfTruth || 'internal', + externalProvider: current.sync?.externalProvider || '', + externalResourceId: current.sync?.externalResourceId || '', + lastDryRunAt: current.sync?.lastDryRunAt || null, + lastDryRunStatus: current.sync?.lastDryRunStatus || '' + } + }; + } + + static inferConflictMeta(availability = {}) { + const reason = availability?.reason || ''; + if (reason.toLowerCase().includes('class')) { + return { conflictType: 'class_schedule_conflict', conflictSource: 'class_schedule' }; + } + if (reason.toLowerCase().includes('event')) { + return { conflictType: 'event_overlap_conflict', conflictSource: 'event_overlap' }; + } + return { conflictType: 'reservation_conflict', conflictSource: 'manual' }; + } + + appendHistoryEntry(eventDoc, action, actorId = null, note = '', metadata = {}) { + const reservation = this.normalizeEventReservation(eventDoc); + reservation.history = Array.isArray(reservation.history) ? reservation.history : []; + reservation.history.push({ + action, + actorId, + at: new Date(), + note: note || '', + metadata: metadata || {} + }); + eventDoc.reservation = reservation; + } + + applyExceptionState(eventDoc, { actorId = null, action = 'acknowledged', note = '', assignedTo = null } = {}) { + const reservation = this.normalizeEventReservation(eventDoc); + if (action === 'resolved') { + reservation.resolutionStatus = 'resolved'; + reservation.resolutionNote = note || reservation.resolutionNote || ''; + reservation.conflictSummary = { hasConflict: false, reason: '' }; + reservation.conflictType = ''; + reservation.conflictSource = ''; + reservation.detectedAt = null; + reservation.assignedTo = assignedTo || null; + this.appendHistoryEntry(eventDoc, 'exception_resolved', actorId, note, { assignedTo }); + eventDoc.reservation = this.normalizeEventReservation(eventDoc); + eventDoc.reservation.resolutionStatus = reservation.resolutionStatus; + eventDoc.reservation.resolutionNote = reservation.resolutionNote; + eventDoc.reservation.conflictSummary = reservation.conflictSummary; + eventDoc.reservation.conflictType = reservation.conflictType; + eventDoc.reservation.conflictSource = reservation.conflictSource; + eventDoc.reservation.detectedAt = reservation.detectedAt; + eventDoc.reservation.assignedTo = reservation.assignedTo; + return; + } + reservation.resolutionStatus = 'acknowledged'; + reservation.resolutionNote = note || reservation.resolutionNote || ''; + reservation.assignedTo = assignedTo || reservation.assignedTo || null; + eventDoc.reservation = reservation; + this.appendHistoryEntry(eventDoc, 'exception_acknowledged', actorId, note, { assignedTo: reservation.assignedTo }); + } + + async listUnresolvedConflicts({ orgId = null, limit = 50 } = {}) { + const { Event } = this.models; + const query = { + isDeleted: false, + 'reservation.conflictSummary.hasConflict': true, + 'reservation.resolutionStatus': { $ne: 'resolved' } + }; + if (orgId) { + query.$or = [{ hostingId: orgId }, { 'collaboratorOrgs.orgId': orgId }]; + } + return Event.find(query) + .select('_id name start_time end_time status location hostingId reservation') + .sort({ 'reservation.detectedAt': -1, updatedAt: -1 }) + .limit(Math.max(1, Math.min(Number(limit) || 50, 250))); + } + + async checkAvailability({ startTime, endTime, resourceId, excludeEventId = null }) { + const { Event, Schedule } = this.models; + if (!resourceId) { + return { isAvailable: true, reason: 'No reservable resource linked' }; + } + const start = ResourceReservationService.parseDate(startTime); + const end = ResourceReservationService.parseDate(endTime); + if (!start || !end || start >= end) { + return { isAvailable: false, reason: 'Invalid time range' }; + } + + const dayIndex = start.getDay(); // sunday=0 + const dayKey = ['U', 'M', 'T', 'W', 'R', 'F', 'S'][dayIndex]; + if (dayKey && dayKey !== 'U' && dayKey !== 'S') { + const schedule = await Schedule.findOne({ classroom_id: resourceId }); + if (schedule?.weekly_schedule?.[dayKey]?.length) { + const startMinutes = start.getHours() * 60 + start.getMinutes(); + const endMinutes = end.getHours() * 60 + end.getMinutes(); + const hasClassConflict = schedule.weekly_schedule[dayKey].some((slot) => + startMinutes < slot.end_time && endMinutes > slot.start_time + ); + if (hasClassConflict) { + return { isAvailable: false, reason: 'Resource has scheduled classes during this time', conflicts: [] }; + } + } + } + + const eventQuery = { + isDeleted: false, + status: { $in: ACTIVE_EVENT_STATUSES }, + $and: [ + { $or: [{ 'reservation.resourceId': resourceId }, { classroom_id: resourceId }] }, + { + $or: [ + { start_time: { $gte: start, $lt: end } }, + { end_time: { $gt: start, $lte: end } }, + { start_time: { $lte: start }, end_time: { $gte: end } } + ] + } + ] + }; + if (excludeEventId) eventQuery._id = { $ne: excludeEventId }; + const conflicts = await Event.find(eventQuery).select('_id name start_time end_time status location classroom_id reservation'); + if (conflicts.length > 0) { + return { isAvailable: false, reason: 'Resource has existing event bookings during this time', conflicts }; + } + return { isAvailable: true }; + } + + async applyAvailabilitySnapshot(eventDoc, { startTime, endTime, resourceId, excludeEventId = null }) { + const availability = await this.checkAvailability({ + startTime, + endTime, + resourceId, + excludeEventId + }); + const reservation = this.normalizeEventReservation(eventDoc); + reservation.resourceId = resourceId || null; + reservation.lastCheckedAt = new Date(); + reservation.conflictSummary = { + hasConflict: !availability.isAvailable, + reason: availability.reason || '' + }; + if (!availability.isAvailable) { + const meta = ResourceReservationService.inferConflictMeta(availability); + reservation.conflictType = meta.conflictType; + reservation.conflictSource = meta.conflictSource; + reservation.detectedAt = new Date(); + reservation.resolutionStatus = 'unresolved'; + } else { + reservation.conflictType = ''; + reservation.conflictSource = ''; + reservation.detectedAt = null; + reservation.resolutionStatus = 'resolved'; + reservation.resolutionNote = ''; + } + if (!availability.isAvailable && reservation.state === 'approved') { + reservation.state = 'requested'; + } + eventDoc.reservation = reservation; + if (resourceId && !eventDoc.classroom_id) { + eventDoc.classroom_id = resourceId; + } + return availability; + } +} + +module.exports = ResourceReservationService; diff --git a/backend/services/rootOperatorUsersService.js b/backend/services/rootOperatorUsersService.js new file mode 100644 index 00000000..ef978575 --- /dev/null +++ b/backend/services/rootOperatorUsersService.js @@ -0,0 +1,185 @@ +/** + * Root / community dashboard: search users and manage operator roles + suspension. + * Uses getModels(req, 'User') only. + */ + +const getModels = require('./getModelService'); + +/** Roles this panel may grant or revoke (end-user operators: admin only). */ +const MANAGEABLE_ROLES = ['admin']; + +function escapeRegex(str) { + return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function actorUserId(req) { + return String(req.user?.userId || req.user?.tenantUserId || ''); +} + +/** + * Snapshot counts for the People & access dashboard header. + * @param {import('express').Request} req + */ +async function getRootOperatorUserStats(req) { + const { User } = getModels(req, 'User'); + const [totalUsers, adminCount] = await Promise.all([ + User.countDocuments({}), + User.countDocuments({ roles: 'admin' }), + ]); + const memberCount = Math.max(0, totalUsers - adminCount); + return { + totalUsers, + adminCount, + memberCount, + }; +} + +/** + * @param {import('express').Request} req + * @param {{ q?: string, limit?: number, role?: string }} opts + */ +async function searchRootOperatorUsers(req, opts = {}) { + const { User } = getModels(req, 'User'); + const rawQ = typeof opts.q === 'string' ? opts.q.trim() : ''; + const limit = Math.min(50, Math.max(1, parseInt(String(opts.limit || 25), 10) || 25)); + const roleFilter = + typeof opts.role === 'string' && opts.role && MANAGEABLE_ROLES.includes(opts.role) ? opts.role : ''; + + if (rawQ.length < 2) { + return { users: [], total: 0 }; + } + + const escaped = escapeRegex(rawQ); + const searchQuery = { + $or: [ + { username: { $regex: escaped, $options: 'i' } }, + { name: { $regex: escaped, $options: 'i' } }, + { email: { $regex: escaped, $options: 'i' } }, + ], + }; + if (roleFilter) { + searchQuery.roles = roleFilter; + } + + const [users, total] = await Promise.all([ + User.find(searchQuery) + .sort({ username: 1 }) + .limit(limit) + .select('username name email picture roles accessSuspended accessSuspendedAt createdAt') + .lean(), + User.countDocuments(searchQuery), + ]); + + return { users, total }; +} + +/** + * @param {import('express').Request} req + * @param {{ userId: string, role: string, assign: boolean }} opts + */ +async function setRootOperatorUserRole(req, opts) { + const actorId = actorUserId(req); + const targetId = String(opts.userId || ''); + const role = opts.role; + const assign = opts.assign === true; + + if (!actorId || !targetId || !MANAGEABLE_ROLES.includes(role)) { + const e = new Error('Invalid request'); + e.statusCode = 400; + throw e; + } + + const { User } = getModels(req, 'User'); + const [actor, target] = await Promise.all([User.findById(actorId), User.findById(targetId)]); + if (!actor || !target) { + const e = new Error('User not found'); + e.statusCode = 404; + throw e; + } + + const actorRoles = new Set(actor.roles || []); + if (!['admin', 'developer', 'beta'].some((r) => actorRoles.has(r))) { + const e = new Error('Forbidden'); + e.statusCode = 403; + throw e; + } + + if (!actorRoles.has('admin')) { + const e = new Error('Only admins can change admin access'); + e.statusCode = 403; + throw e; + } + + if (!assign && role === 'admin' && (target.roles || []).includes('admin')) { + const adminCount = await User.countDocuments({ roles: 'admin' }); + if (adminCount <= 1) { + const e = new Error('Cannot remove the last admin'); + e.statusCode = 400; + throw e; + } + } + + let nextRoles = [...(target.roles || [])]; + if (assign) { + if (!nextRoles.includes(role)) nextRoles.push(role); + } else { + nextRoles = nextRoles.filter((r) => r !== role); + } + if (nextRoles.length === 0) nextRoles = ['user']; + else if (!nextRoles.includes('user')) nextRoles.push('user'); + + target.roles = nextRoles; + await target.save(); + + return { roles: target.roles }; +} + +/** + * @param {import('express').Request} req + * @param {{ userId: string, accessSuspended: boolean }} opts + */ +async function setRootOperatorAccessSuspended(req, opts) { + const actorId = actorUserId(req); + const targetId = String(opts.userId || ''); + const suspended = Boolean(opts.accessSuspended); + + if (!actorId || !targetId) { + const e = new Error('Invalid request'); + e.statusCode = 400; + throw e; + } + if (actorId === targetId) { + const e = new Error('You cannot change suspension on your own account'); + e.statusCode = 400; + throw e; + } + + const { User } = getModels(req, 'User'); + const actor = await User.findById(actorId); + if (!actor?.roles?.includes('admin')) { + const e = new Error('Only admins can suspend or restore accounts'); + e.statusCode = 403; + throw e; + } + + const target = await User.findById(targetId); + if (!target) { + const e = new Error('User not found'); + e.statusCode = 404; + throw e; + } + + target.accessSuspended = suspended; + target.accessSuspendedAt = suspended ? new Date() : null; + await target.save(); + + return { accessSuspended: target.accessSuspended, accessSuspendedAt: target.accessSuspendedAt }; +} + +module.exports = { + getRootOperatorUserStats, + searchRootOperatorUsers, + setRootOperatorUserRole, + setRootOperatorAccessSuspended, + MANAGEABLE_ROLES, +}; diff --git a/backend/services/studySessionService.js b/backend/services/studySessionService.js index 07b109c4..e8da3030 100644 --- a/backend/services/studySessionService.js +++ b/backend/services/studySessionService.js @@ -1,11 +1,13 @@ const getModels = require('./getModelService'); const FeedbackService = require('./feedbackService'); +const ResourceReservationService = require('./resourceReservationService'); class StudySessionService { constructor(req) { this.req = req; this.models = getModels(req, 'StudySession', 'Event', 'User', 'Classroom', 'Schedule', 'AvailabilityPoll', 'Notification'); this.feedbackService = new FeedbackService(req); + this.resourceReservationService = new ResourceReservationService(req); } // Get current semester end date ß @@ -43,6 +45,7 @@ class StudySessionService { hostingId: userId, hostingType: "User", location: sessionData.location, + classroom_id: sessionData.classroomId || null, start_time: new Date(sessionData.startTime), end_time: new Date(sessionData.endTime), description: sessionData.description || `Study session for ${sessionData.course}`, @@ -56,6 +59,10 @@ class StudySessionService { isStudySession: true, // Flag for filtering isDeleted: false }; + eventData.reservation = this.resourceReservationService.normalizeEventReservation({ + classroom_id: eventData.classroom_id, + status: eventData.status + }); const event = new Event(eventData); await event.save(); @@ -138,6 +145,7 @@ class StudySessionService { hostingId: userId, hostingType: "User", location: session.location, + classroom_id: session.classroomId || null, start_time: new Date(session.startTime), end_time: new Date(session.endTime), description: session.description || `Study session for ${session.course}`, @@ -151,6 +159,10 @@ class StudySessionService { isStudySession: true, isDeleted: false }; + eventData.reservation = this.resourceReservationService.normalizeEventReservation({ + classroom_id: eventData.classroom_id, + status: eventData.status + }); const event = new Event(eventData); await event.save(); @@ -257,67 +269,15 @@ class StudySessionService { // Check room availability for study session async checkRoomAvailability(startTime, endTime, roomName, excludeEventId = null) { - const { Schedule, Event, Classroom } = this.models; - - const start = new Date(startTime); - const end = new Date(endTime); - - // Check if room exists and is not restricted + const { Classroom } = this.models; const room = await Classroom.findOne({ name: roomName }); - if (!room) { - return { isAvailable: true, }; - } - - // if (room.attributes && room.attributes.includes('restricted')) { - // return { isAvailable: false, reason: 'Room is restricted' }; - // } - - // Check classroom schedule conflicts - const dayOfWeek = ['M', 'T', 'W', 'R', 'F'][start.getDay() - 1]; // Monday = 0 - if (dayOfWeek) { - const schedule = await Schedule.findOne({ classroom_id: room._id }); - if (schedule && schedule.weekly_schedule[dayOfWeek]) { - const startMinutes = start.getHours() * 60 + start.getMinutes(); - const endMinutes = end.getHours() * 60 + end.getMinutes(); - - const hasClassConflict = schedule.weekly_schedule[dayOfWeek].some(classTime => { - return startMinutes < classTime.end_time && endMinutes > classTime.start_time; - }); - - if (hasClassConflict) { - return { isAvailable: false, reason: 'Room has scheduled classes during this time' }; - } - } - } - - // Check event conflicts (including other study sessions) - const eventQuery = { - location: roomName, - $or: [ - { start_time: { $gte: start, $lt: end } }, - { end_time: { $gt: start, $lte: end } }, - { start_time: { $lte: start }, end_time: { $gte: end } } - ], - status: { $in: ['approved', 'not-applicable'] }, - isDeleted: false - }; - - // Exclude current event if provided - if (excludeEventId) { - eventQuery._id = { $ne: excludeEventId }; - } - - const eventConflicts = await Event.find(eventQuery); - - if (eventConflicts.length > 0) { - return { - isAvailable: false, - reason: 'Room has existing event bookings during this time', - conflicts: eventConflicts - }; - } - - return { isAvailable: true }; + if (!room) return { isAvailable: true }; + return this.resourceReservationService.checkAvailability({ + startTime, + endTime, + resourceId: room._id, + excludeEventId + }); } // Check room availability by classroom_id (for events) @@ -333,7 +293,12 @@ class StudySessionService { return { isAvailable: false, reason: 'Room not found' }; } - return this.checkRoomAvailability(startTime, endTime, room.name, excludeEventId); + return this.resourceReservationService.checkAvailability({ + startTime, + endTime, + resourceId: room._id, + excludeEventId + }); } // Get suggested rooms for a time slot diff --git a/backend/services/taskBoardStatusUtils.js b/backend/services/taskBoardStatusUtils.js new file mode 100644 index 00000000..2aef912a --- /dev/null +++ b/backend/services/taskBoardStatusUtils.js @@ -0,0 +1,140 @@ +const MAX_TASK_BOARD_STATUSES = 10; + +const DEFAULT_TASK_BOARD_STATUSES = [ + { key: 'todo', label: 'To do', category: 'backlog', order: 0 }, + { key: 'in_progress', label: 'In progress', category: 'active', order: 1 }, + { key: 'done', label: 'Done', category: 'done', order: 2 } +]; + +const LEGACY_STATUS_CATEGORY = { + todo: 'backlog', + in_progress: 'active', + blocked: 'active', + done: 'done', + cancelled: 'cancelled' +}; + +function getResolvedTaskBoardStatuses(orgLike) { + const raw = orgLike?.taskBoardStatuses; + if (Array.isArray(raw) && raw.length > 0) { + return raw + .map((s, i) => ({ + key: String(s.key || '') + .toLowerCase() + .trim(), + label: (String(s.label || s.key || '').trim() || s.key || '').slice(0, 64), + category: s.category, + order: Number.isFinite(s.order) ? s.order : i + })) + .filter( + (s) => + s.key && + s.label && + ['backlog', 'active', 'done', 'cancelled'].includes(s.category) + ) + .sort((a, b) => a.order - b.order || a.key.localeCompare(b.key)) + .slice(0, MAX_TASK_BOARD_STATUSES); + } + return DEFAULT_TASK_BOARD_STATUSES.map((s) => ({ ...s })); +} + +function getAllowedStatusKeys(statuses) { + return new Set((statuses || []).map((s) => s.key)); +} + +function resolveStatusCategory(taskStatus, statuses) { + const key = String(taskStatus || '').toLowerCase(); + const row = (statuses || []).find((s) => s.key === key); + if (row) return row.category; + return LEGACY_STATUS_CATEGORY[key] || 'backlog'; +} + +function pickDefaultOpenKey(statuses) { + const cfg = statuses || DEFAULT_TASK_BOARD_STATUSES; + const b = cfg.find((s) => s.category === 'backlog'); + if (b) return b.key; + const a = cfg.find((s) => s.category === 'active'); + return a?.key || 'todo'; +} + +function pickDefaultActiveKey(statuses) { + const cfg = statuses || DEFAULT_TASK_BOARD_STATUSES; + const a = cfg.find((s) => s.category === 'active'); + return a?.key || 'in_progress'; +} + +function pickFirstDoneKey(statuses) { + const cfg = statuses || DEFAULT_TASK_BOARD_STATUSES; + const d = cfg.find((s) => s.category === 'done'); + return d?.key || 'done'; +} + +function normalizeTaskStatusForOrg(status, statuses) { + const cfg = statuses || DEFAULT_TASK_BOARD_STATUSES; + const allowed = getAllowedStatusKeys(cfg); + const normalized = String(status || '').toLowerCase(); + if (normalized === 'blocked') { + return pickDefaultActiveKey(cfg); + } + if (allowed.has(normalized)) return normalized; + return pickDefaultOpenKey(cfg); +} + +const KEY_REGEX = /^[a-z][a-z0-9_]{0,39}$/; + +function validateTaskBoardStatusesPayload(statuses) { + if (!Array.isArray(statuses)) { + return { error: 'statuses must be an array' }; + } + if (statuses.length < 1 || statuses.length > MAX_TASK_BOARD_STATUSES) { + return { error: `Provide between 1 and ${MAX_TASK_BOARD_STATUSES} columns` }; + } + const seen = new Set(); + let hasDone = false; + let hasOpen = false; + const value = []; + for (let i = 0; i < statuses.length; i += 1) { + const s = statuses[i] || {}; + const key = String(s.key || '') + .toLowerCase() + .trim(); + const label = String(s.label || '').trim(); + const category = s.category; + if (!KEY_REGEX.test(key)) { + return { error: `Invalid key "${key}" (use lowercase letters, numbers, underscores; max 40 chars)` }; + } + if (seen.has(key)) { + return { error: `Duplicate key: ${key}` }; + } + seen.add(key); + if (!label || label.length > 64) { + return { error: 'Each column needs a label (1–64 characters)' }; + } + if (!['backlog', 'active', 'done', 'cancelled'].includes(category)) { + return { error: 'Invalid category' }; + } + if (category === 'done') hasDone = true; + if (category === 'backlog' || category === 'active') hasOpen = true; + value.push({ key, label, category, order: i }); + } + if (!hasDone) { + return { error: 'At least one column must have category "done"' }; + } + if (!hasOpen) { + return { error: 'At least one column must be backlog or active' }; + } + return { value }; +} + +module.exports = { + MAX_TASK_BOARD_STATUSES, + DEFAULT_TASK_BOARD_STATUSES, + getResolvedTaskBoardStatuses, + getAllowedStatusKeys, + resolveStatusCategory, + pickDefaultOpenKey, + pickDefaultActiveKey, + pickFirstDoneKey, + normalizeTaskStatusForOrg, + validateTaskBoardStatusesPayload +}; diff --git a/backend/services/taskService.js b/backend/services/taskService.js new file mode 100644 index 00000000..d0e29a3b --- /dev/null +++ b/backend/services/taskService.js @@ -0,0 +1,594 @@ +const mongoose = require('mongoose'); +const { + DEFAULT_TASK_BOARD_STATUSES, + getResolvedTaskBoardStatuses, + resolveStatusCategory, + normalizeTaskStatusForOrg, + pickFirstDoneKey, + pickDefaultActiveKey +} = require('./taskBoardStatusUtils'); + +const PRIORITY_WEIGHTS = { + low: 1, + medium: 2, + high: 3, + critical: 4 +}; + +const STATUS_SORT = { + blocked: 0, + in_progress: 1, + todo: 2, + done: 3, + cancelled: 4 +}; + +const TASK_SUGGESTION_LIBRARY = { + default: [ + { key: 'kickoff', title: 'Run kickoff alignment', description: 'Confirm scope, owners, and timeline.', priority: 'high', status: 'todo', isCritical: true, dueRule: { anchorType: 'event_start', offsetValue: 21, offsetUnit: 'days', direction: 'before' } }, + { key: 'promotion', title: 'Publish promotion plan', description: 'Announce event channels and cadence.', priority: 'medium', status: 'todo', isCritical: false, dueRule: { anchorType: 'event_start', offsetValue: 14, offsetUnit: 'days', direction: 'before' } }, + { key: 'staffing', title: 'Confirm staffing coverage', description: 'Assign event-day roles and backups.', priority: 'high', status: 'todo', isCritical: true, dueRule: { anchorType: 'event_start', offsetValue: 7, offsetUnit: 'days', direction: 'before' } }, + { key: 'runbook', title: 'Finalize day-of runbook', description: 'Prepare timeline, contacts, and contingency notes.', priority: 'high', status: 'todo', isCritical: true, dueRule: { anchorType: 'event_start', offsetValue: 2, offsetUnit: 'days', direction: 'before' } }, + { key: 'retro', title: 'Post-event debrief', description: 'Capture outcomes and follow-up actions.', priority: 'medium', status: 'todo', isCritical: false, dueRule: { anchorType: 'event_end', offsetValue: 2, offsetUnit: 'days', direction: 'after' } } + ], + workshop: [ + { key: 'materials', title: 'Prepare workshop materials', description: 'Slides, handouts, and activity assets.', priority: 'high', status: 'todo', isCritical: true, dueRule: { anchorType: 'event_start', offsetValue: 5, offsetUnit: 'days', direction: 'before' } } + ], + social: [ + { key: 'guest-flow', title: 'Plan guest flow and check-in', description: 'Entry process, check-in points, and host assignments.', priority: 'medium', status: 'todo', isCritical: false, dueRule: { anchorType: 'event_start', offsetValue: 3, offsetUnit: 'days', direction: 'before' } } + ] +}; + +function asObjectId(id) { + if (!id) return null; + if (id instanceof mongoose.Types.ObjectId) return id; + if (mongoose.Types.ObjectId.isValid(id)) return new mongoose.Types.ObjectId(id); + return null; +} + +function addOffset(baseDate, offsetValue, offsetUnit, direction) { + const date = new Date(baseDate); + const sign = direction === 'after' ? 1 : -1; + const amount = Number(offsetValue || 0) * sign; + switch (offsetUnit) { + case 'minutes': + date.setMinutes(date.getMinutes() + amount); + break; + case 'hours': + date.setHours(date.getHours() + amount); + break; + case 'weeks': + date.setDate(date.getDate() + (amount * 7)); + break; + case 'days': + default: + date.setDate(date.getDate() + amount); + break; + } + return date; +} + +function computeDueAtForTask(task, event, approvalAnchorDate = null) { + const dueRule = task?.dueRule || {}; + if (!dueRule || dueRule.anchorType === 'none') return null; + if (dueRule.anchorType === 'absolute') { + return dueRule.absoluteDate ? new Date(dueRule.absoluteDate) : null; + } + + let anchorDate = null; + if (dueRule.anchorType === 'event_start') { + anchorDate = event?.start_time ? new Date(event.start_time) : null; + } else if (dueRule.anchorType === 'event_end') { + anchorDate = event?.end_time ? new Date(event.end_time) : null; + } else if (dueRule.anchorType === 'approval_granted') { + anchorDate = approvalAnchorDate ? new Date(approvalAnchorDate) : null; + } + + if (!anchorDate) return null; + return addOffset(anchorDate, dueRule.offsetValue, dueRule.offsetUnit, dueRule.direction); +} + +async function resolveApprovalAnchorDate(models, eventId) { + if (!eventId || !models?.ApprovalInstance) return null; + try { + const approval = await models.ApprovalInstance.findOne({ + eventId: asObjectId(eventId), + status: 'approved' + }).sort({ updatedAt: -1 }).lean(); + return approval?.updatedAt || null; + } catch (_error) { + return null; + } +} + +function buildTaskSort(sortBy = 'priority') { + const boardTie = { boardRank: 1 }; + if (sortBy === 'dueAt') return { dueAt: 1, priority: -1, createdAt: -1, ...boardTie }; + if (sortBy === 'createdAt') return { createdAt: -1, ...boardTie }; + if (sortBy === 'status') return { status: 1, priority: -1, dueAt: 1, createdAt: -1, ...boardTie }; + return { priority: -1, dueAt: 1, createdAt: -1, ...boardTie }; +} + +function toTaskDto(task, statusConfig = DEFAULT_TASK_BOARD_STATUSES) { + const plain = task.toObject ? task.toObject() : task; + const blockedByUnresolved = (plain.blockers || []).some((blocker) => !blocker.resolved); + const cat = resolveStatusCategory(plain.status, statusConfig); + const isDone = cat === 'done'; + const isCancelled = cat === 'cancelled'; + const effectiveStatus = + blockedByUnresolved && !isDone && !isCancelled ? 'blocked' : plain.status; + + return { + ...plain, + effectiveStatus, + priorityWeight: PRIORITY_WEIGHTS[plain.priority] || PRIORITY_WEIGHTS.medium, + overdue: Boolean( + plain.dueAt && new Date(plain.dueAt) < new Date() && !isDone && !isCancelled + ) + }; +} + +function computeUrgencyScore(taskDto) { + const now = Date.now(); + const dueAtMs = taskDto.dueAt ? new Date(taskDto.dueAt).getTime() : null; + const priority = PRIORITY_WEIGHTS[taskDto.priority] || PRIORITY_WEIGHTS.medium; + const blockedPenalty = taskDto.effectiveStatus === 'blocked' ? 35 : 0; + const criticalBoost = taskDto.isCritical ? 25 : 0; + + let dueScore = 0; + if (dueAtMs) { + const daysUntilDue = (dueAtMs - now) / (1000 * 60 * 60 * 24); + if (daysUntilDue < 0) { + dueScore = 50 + Math.min(30, Math.abs(daysUntilDue) * 5); + } else if (daysUntilDue <= 1) { + dueScore = 35; + } else if (daysUntilDue <= 3) { + dueScore = 25; + } else if (daysUntilDue <= 7) { + dueScore = 15; + } else { + dueScore = 8; + } + } + + return (priority * 12) + dueScore + criticalBoost + blockedPenalty; +} + +function scoreBand(score) { + if (score >= 85) return 'ready'; + if (score >= 65) return 'on_track'; + if (score >= 40) return 'at_risk'; + return 'not_ready'; +} + +/** @deprecated Use normalizeTaskStatusForOrg from taskBoardStatusUtils with org config */ +function normalizeTaskStatus(status) { + return normalizeTaskStatusForOrg(status, DEFAULT_TASK_BOARD_STATUSES); +} + +function getSuggestedTasksForEvent(event = {}, options = {}) { + const eventType = String(options.eventType || event.type || '').toLowerCase(); + const defaults = TASK_SUGGESTION_LIBRARY.default || []; + const typeSpecific = TASK_SUGGESTION_LIBRARY[eventType] || []; + return [...defaults, ...typeSpecific].map((suggestion) => ({ + ...suggestion, + status: normalizeTaskStatus(suggestion.status || 'todo'), + source: 'template_suggestion', + userConfirmed: false + })); +} + +async function computeEventReadiness(models, orgId, eventId) { + const eventObjectId = asObjectId(eventId); + if (!eventObjectId) return null; + + const orgObjectId = asObjectId(orgId); + const orgDoc = models.Org && orgObjectId + ? await models.Org.findById(orgObjectId).select('taskBoardStatuses').lean() + : null; + const statusConfig = getResolvedTaskBoardStatuses(orgDoc); + + const [event, tasks, approvalInstances, equipment, roles] = await Promise.all([ + models.Event.findOne({ _id: eventObjectId, hostingType: 'Org', isDeleted: false }).lean(), + models.Task.find({ orgId: orgObjectId, eventId: eventObjectId }).lean(), + models.ApprovalInstance + ? models.ApprovalInstance.find({ eventId: eventObjectId }).lean() + : Promise.resolve([]), + models.EventEquipment + ? models.EventEquipment.findOne({ eventId: eventObjectId }).lean() + : Promise.resolve(null), + models.EventJob + ? models.EventJob.find({ eventId: eventObjectId }).lean() + : Promise.resolve([]) + ]); + + if (!event) return null; + + const actionableTasks = tasks.filter( + (task) => resolveStatusCategory(task.status, statusConfig) !== 'cancelled' + ); + const totalTaskWeight = actionableTasks.reduce((sum, task) => sum + Math.max(0, Number(task?.readinessContribution?.weight ?? 1)), 0); + const doneTaskWeight = actionableTasks + .filter((task) => resolveStatusCategory(task.status, statusConfig) === 'done') + .reduce((sum, task) => sum + Math.max(0, Number(task?.readinessContribution?.weight ?? 1)), 0); + const weightedBlockedTasks = actionableTasks.filter((task) => + task?.readinessContribution?.blocked || (task.blockers || []).some((blocker) => !blocker.resolved) + ); + const blockedWeightPenalty = totalTaskWeight > 0 + ? Math.min( + 0.35, + weightedBlockedTasks.reduce( + (sum, task) => sum + Math.max(0, Number(task?.readinessContribution?.weight ?? 1)), + 0 + ) / totalTaskWeight + ) + : 0; + const taskCompletionRaw = totalTaskWeight > 0 ? (doneTaskWeight / totalTaskWeight) : 0; + const taskCompletion = Math.max(0, taskCompletionRaw - blockedWeightPenalty); + + const criticalIncomplete = actionableTasks.filter((task) => { + const c = resolveStatusCategory(task.status, statusConfig); + return task.isCritical && c !== 'done' && c !== 'cancelled'; + }); + const blockedCritical = criticalIncomplete.filter((task) => + (task.blockers || []).some((blocker) => !blocker.resolved) + ); + + const approvalsRequired = approvalInstances.length > 0; + const approvalsApproved = approvalInstances.filter((instance) => instance.status === 'approved').length; + const approvalsScore = approvalsRequired + ? approvalsApproved / approvalInstances.length + : 1; + + const hasEquipment = Boolean(equipment?.items?.length); + const roleCount = roles.length; + const roleCoverage = roles.length === 0 + ? 1 + : roles.filter((role) => (role.assignments || []).length >= (role.requiredCount || 1)).length / roles.length; + const logisticsScoreRaw = (roleCoverage * 0.7) + ((hasEquipment || roleCount === 0) ? 0.3 : 0); + + const registrationGoal = Number(event.expectedAttendance || 0); + const registrations = Number(event.registrationCount || event.attendees?.length || 0); + const engagementScore = registrationGoal > 0 ? Math.min(1, registrations / registrationGoal) : 1; + + const weightedScore = ( + (taskCompletion * 0.40) + + (approvalsScore * 0.25) + + (logisticsScoreRaw * 0.20) + + (engagementScore * 0.15) + ) * 100; + + const hardBlockers = []; + if (blockedCritical.length > 0) { + hardBlockers.push({ + type: 'critical_blocked_tasks', + label: `${blockedCritical.length} critical task(s) blocked`, + ownerUserIds: blockedCritical.map((task) => task.ownerUserId).filter(Boolean) + }); + } + const pendingApprovals = approvalInstances.filter((instance) => instance.status !== 'approved'); + if (pendingApprovals.length > 0) { + hardBlockers.push({ + type: 'approvals_pending', + label: `${pendingApprovals.length} approval item(s) unresolved` + }); + } + if (new Date(event.start_time).getTime() - Date.now() <= (72 * 60 * 60 * 1000) && criticalIncomplete.length > 0) { + hardBlockers.push({ + type: 'critical_near_deadline', + label: 'Critical tasks unresolved with less than 72h to event start' + }); + } + + const cappedScore = hardBlockers.length > 0 ? Math.min(weightedScore, 64.9) : weightedScore; + + return { + score: Math.round(cappedScore * 10) / 10, + band: scoreBand(cappedScore), + dimensions: { + taskCompletion: Math.round(taskCompletion * 100), + approvals: Math.round(approvalsScore * 100), + logistics: Math.round(logisticsScoreRaw * 100), + engagementReadiness: Math.round(engagementScore * 100) + }, + blockers: hardBlockers, + explainability: { + weightedTaskCoverage: Math.round(taskCompletionRaw * 100), + blockedWeightPenalty: Math.round(blockedWeightPenalty * 100), + criticalIncompleteCount: criticalIncomplete.length, + pendingApprovals: pendingApprovals.length + }, + missing: [ + ...criticalIncomplete.map((task) => ({ + type: 'task', + taskId: task._id, + label: task.title, + ownerUserId: task.ownerUserId || null + })) + ] + }; +} + +async function recomputeDueDatesForEvent(models, orgId, eventId) { + const eventObjectId = asObjectId(eventId); + const orgObjectId = asObjectId(orgId); + if (!eventObjectId || !orgObjectId) return 0; + + const [event, approvalAnchorDate, tasks] = await Promise.all([ + models.Event.findOne({ _id: eventObjectId, hostingType: 'Org', isDeleted: false }).lean(), + resolveApprovalAnchorDate(models, eventObjectId), + models.Task.find({ orgId: orgObjectId, eventId: eventObjectId }) + ]); + + if (!event || tasks.length === 0) return 0; + + let updates = 0; + await Promise.all(tasks.map(async (task) => { + const nextDueAt = computeDueAtForTask(task, event, approvalAnchorDate); + const hasChanged = String(task.dueAt || '') !== String(nextDueAt || ''); + if (hasChanged) { + task.dueAt = nextDueAt; + await task.save(); + updates += 1; + } + })); + return updates; +} + +async function listTasks(models, orgId, options = {}) { + const orgObjectId = asObjectId(orgId); + if (!orgObjectId) return []; + + const orgDoc = + models.Org && orgObjectId + ? await models.Org.findById(orgObjectId).select('taskBoardStatuses').lean() + : null; + const statusConfig = getResolvedTaskBoardStatuses(orgDoc); + + const query = { orgId: orgObjectId }; + if (options.eventId === null || options.eventId === 'null') { + query.eventId = null; + } else if (options.eventId) { + query.eventId = asObjectId(options.eventId); + } + if (options.status && options.status !== 'all') { + query.status = options.status; + } + if (options.ownerUserId && options.ownerUserId !== 'unassigned') { + query.ownerUserId = asObjectId(options.ownerUserId); + } else if (options.ownerUserId === 'unassigned') { + query.ownerUserId = null; + } + if (options.priority && options.priority !== 'all') { + query.priority = options.priority; + } + if (options.search) { + query.$or = [ + { title: { $regex: options.search, $options: 'i' } }, + { description: { $regex: options.search, $options: 'i' } } + ]; + } + + const sort = buildTaskSort(options.sortBy); + const tasks = await models.Task.find(query) + .sort(sort) + .populate('ownerUserId', 'name username picture') + .populate('eventId', 'name start_time end_time') + .lean(); + + const taskDtos = tasks.map((t) => toTaskDto(t, statusConfig)).map((task) => ({ + ...task, + urgencyScore: computeUrgencyScore(task) + })); + + if (options.onlyOverdue) { + return taskDtos.filter((task) => task.overdue); + } + if (options.onlyBlocked) { + return taskDtos.filter((task) => task.effectiveStatus === 'blocked'); + } + return taskDtos; +} + +/** + * Unique task owners per event for org (stable order: first task appearance). + * Returns plain object: { [eventId]: [{ _id, name, username, picture }, ...] } + */ +async function buildEventTaskAssigneeSummary(models, orgId) { + const orgObjectId = asObjectId(orgId); + if (!orgObjectId || !models?.Task || !models?.User) { + return {}; + } + + const tasks = await models.Task.find({ + orgId: orgObjectId, + eventId: { $ne: null }, + ownerUserId: { $ne: null } + }) + .select('eventId ownerUserId') + .sort({ updatedAt: 1 }) + .lean(); + + const byEvent = new Map(); + for (const t of tasks) { + const eid = t.eventId != null ? String(t.eventId) : ''; + const uid = t.ownerUserId != null ? String(t.ownerUserId) : ''; + if (!eid || !uid) continue; + if (!byEvent.has(eid)) byEvent.set(eid, []); + const arr = byEvent.get(eid); + if (!arr.includes(uid)) arr.push(uid); + } + + const allIds = [...new Set([].concat(...byEvent.values()))]; + if (!allIds.length) return {}; + + const oids = allIds.map((id) => asObjectId(id)).filter(Boolean); + const users = await models.User.find({ _id: { $in: oids } }) + .select('name username picture') + .lean(); + const userMap = new Map( + users.map((u) => [ + String(u._id), + { _id: u._id, name: u.name, username: u.username, picture: u.picture } + ]) + ); + + const assigneesByEventId = {}; + for (const [eid, uids] of byEvent.entries()) { + assigneesByEventId[eid] = uids.map((id) => userMap.get(id)).filter(Boolean); + } + return assigneesByEventId; +} + +async function findOneTaskDto(models, orgId, taskId, eventIdConstraint = null) { + const orgObjectId = asObjectId(orgId); + const taskObjectId = asObjectId(taskId); + if (!orgObjectId || !taskObjectId || !models?.Task) return null; + + const orgDoc = + models.Org && orgObjectId + ? await models.Org.findById(orgObjectId).select('taskBoardStatuses').lean() + : null; + const statusConfig = getResolvedTaskBoardStatuses(orgDoc); + + const query = { _id: taskObjectId, orgId: orgObjectId }; + if (eventIdConstraint) { + query.eventId = asObjectId(eventIdConstraint); + } + + const task = await models.Task.findOne(query) + .populate('ownerUserId', 'name username picture') + .populate('eventId', 'name start_time end_time') + .lean(); + if (!task) return null; + + const dto = toTaskDto(task, statusConfig); + return { + ...dto, + urgencyScore: computeUrgencyScore(dto) + }; +} + +function sortHubTasks(tasks, sortBy = 'urgency') { + const byBoardRank = (a, b) => { + const d = (Number(a.boardRank) || 0) - (Number(b.boardRank) || 0); + if (d !== 0) return d; + return (new Date(a.createdAt).getTime()) - (new Date(b.createdAt).getTime()); + }; + const cloned = [...tasks]; + if (sortBy === 'dueAt') { + return cloned.sort((a, b) => { + const aDue = a.dueAt ? new Date(a.dueAt).getTime() : Number.MAX_SAFE_INTEGER; + const bDue = b.dueAt ? new Date(b.dueAt).getTime() : Number.MAX_SAFE_INTEGER; + const byDue = aDue - bDue; + if (byDue !== 0) return byDue; + return byBoardRank(a, b); + }); + } + if (sortBy === 'priority') { + return cloned.sort((a, b) => { + const byPriority = (b.priorityWeight || 0) - (a.priorityWeight || 0); + if (byPriority !== 0) return byPriority; + const byStatus = (STATUS_SORT[a.effectiveStatus] ?? 10) - (STATUS_SORT[b.effectiveStatus] ?? 10); + if (byStatus !== 0) return byStatus; + return byBoardRank(a, b); + }); + } + return cloned.sort((a, b) => { + const byU = (b.urgencyScore || 0) - (a.urgencyScore || 0); + if (byU !== 0) return byU; + return byBoardRank(a, b); + }); +} + +/** + * Persist 0..n-1 boardRank for tasks in one column (org-scoped; optional event scope). + * @param {import('mongoose').Model} models + * @param {string} orgId + * @param {string[]} taskIdsOrdered + * @param {string|null|undefined} eventId - if set, only tasks for this event; if null, only hub tasks (eventId null); if undefined, any event under org + */ +async function applyTaskColumnOrder(models, orgId, taskIdsOrdered, eventId) { + const orgOid = asObjectId(orgId); + if (!orgOid || !models?.Task || !Array.isArray(taskIdsOrdered)) { + return { updated: 0 }; + } + let updated = 0; + for (let i = 0; i < taskIdsOrdered.length; i += 1) { + const id = asObjectId(taskIdsOrdered[i]); + if (!id) continue; + const q = { _id: id, orgId: orgOid }; + if (eventId !== undefined) { + q.eventId = eventId ? asObjectId(eventId) : null; + } + const res = await models.Task.updateOne(q, { $set: { boardRank: i } }); + if (res.modifiedCount || res.matchedCount) updated += 1; + } + return { updated }; +} + +module.exports = { + PRIORITY_WEIGHTS, + asObjectId, + applyTaskColumnOrder, + computeDueAtForTask, + computeEventReadiness, + getSuggestedTasksForEvent, + normalizeTaskStatus, + normalizeTaskStatusForOrg, + getResolvedTaskBoardStatuses, + pickFirstDoneKey, + pickDefaultActiveKey, + DEFAULT_TASK_BOARD_STATUSES, + syncApprovalLinkedTasks: async (models, orgId, eventId, approvalInstanceId, approvalStatus = 'pending') => { + const orgObjectId = asObjectId(orgId); + const eventObjectId = asObjectId(eventId); + if (!orgObjectId || !eventObjectId || !approvalInstanceId || !models?.Task) return 0; + const tasks = await models.Task.find({ + orgId: orgObjectId, + eventId: eventObjectId, + integrationLinks: { + $elemMatch: { + type: 'approval_instance', + referenceId: String(approvalInstanceId) + } + } + }); + if (!tasks.length) return 0; + const shouldResolve = approvalStatus === 'approved'; + let updates = 0; + await Promise.all(tasks.map(async (task) => { + let changed = false; + task.integrationLinks = (task.integrationLinks || []).map((link) => { + if (link.type !== 'approval_instance' || String(link.referenceId) !== String(approvalInstanceId)) { + return link; + } + if (link.status !== approvalStatus) { + changed = true; + return { ...link, status: approvalStatus }; + } + return link; + }); + task.blockers = (task.blockers || []).map((blocker) => { + if (blocker.type !== 'approval' || String(blocker.referenceId) !== String(approvalInstanceId)) { + return blocker; + } + if (blocker.resolved !== shouldResolve) { + changed = true; + return { ...blocker, resolved: shouldResolve }; + } + return blocker; + }); + if (changed) { + await task.save(); + updates += 1; + } + })); + return updates; + }, + recomputeDueDatesForEvent, + listTasks, + findOneTaskDto, + sortHubTasks, + buildEventTaskAssigneeSummary +}; diff --git a/backend/services/userServices.js b/backend/services/userServices.js index 4b88db09..d34c9808 100644 --- a/backend/services/userServices.js +++ b/backend/services/userServices.js @@ -81,6 +81,9 @@ async function loginUser({ email, password, req }) { if (!passwordMatch) { throw new Error('Invalid credentials'); } + if (user.accessSuspended) { + throw new Error('This account has been suspended'); + } delete user.password; return { user }; } diff --git a/backend/tests/route-outcomes/orgBudgetRoutes.outcomes.test.js b/backend/tests/route-outcomes/orgBudgetRoutes.outcomes.test.js new file mode 100644 index 00000000..8fda235e --- /dev/null +++ b/backend/tests/route-outcomes/orgBudgetRoutes.outcomes.test.js @@ -0,0 +1,146 @@ +/** + * Org budget routes — create, submit, org-stage approve, CSV export (multi-tenant req.db). + */ +const express = require('express'); +const request = require('supertest'); +const mongoose = require('mongoose'); + +const { createMongoMemoryConnection, getOrCreateModel } = require('../helpers/mongoMemory'); +const financeConfigSchema = require('../../schemas/financeConfig'); +const orgBudgetSchema = require('../../schemas/orgBudget'); +const orgSchema = require('../../schemas/org'); +const userSchema = require('../../schemas/user'); + +let testUserId; +let mongo; + +jest.mock('../../middlewares/verifyToken', () => ({ + verifyToken: (req, res, next) => { + req.user = { userId: testUserId }; + next(); + } +})); + +jest.mock('../../middlewares/orgPermissions', () => ({ + requireOrgPermission: () => (req, res, next) => next() +})); + +jest.mock('../../services/getModelService', () => (req, ...names) => { + const models = { + FinanceConfig: getOrCreateModel(req.db, 'FinanceConfig', financeConfigSchema, 'financeConfigs'), + OrgBudget: getOrCreateModel(req.db, 'OrgBudget', orgBudgetSchema, 'orgBudgets'), + Org: getOrCreateModel(req.db, 'Org', orgSchema, 'orgs'), + User: getOrCreateModel(req.db, 'User', userSchema, 'users') + }; + return names.reduce((acc, n) => { + if (models[n]) acc[n] = models[n]; + return acc; + }, {}); +}); + +const orgBudgetRoutes = require('../../routes/orgBudgetRoutes'); +const budgetService = require('../../services/budgetService'); + +describe('org budget route outcomes', () => { + let app; + let orgId; + + beforeAll(async () => { + mongo = await createMongoMemoryConnection(); + app = express(); + app.use(express.json()); + app.use((req, _res, next) => { + req.db = mongo.connection; + req.school = 'rpi'; + next(); + }); + app.use('/org-budgets', orgBudgetRoutes); + }); + + afterAll(async () => { + await mongo.cleanup(); + }); + + beforeEach(async () => { + await mongo.reset(); + const UserModel = getOrCreateModel(mongo.connection, 'User', userSchema, 'users'); + const OrgModel = getOrCreateModel(mongo.connection, 'Org', orgSchema, 'orgs'); + const u = await UserModel.create({ + email: `u-${new mongoose.Types.ObjectId().toString()}@test.local`, + name: 'Tester' + }); + testUserId = u._id; + const o = await OrgModel.create({ + org_name: 'Test Org', + org_profile_image: 'https://example.com/p.png', + org_description: 'd', + owner: u._id, + orgTypeKey: 'club' + }); + orgId = o._id.toString(); + await budgetService.ensureFinanceConfig({ db: mongo.connection, school: 'rpi' }); + }); + + test('POST create, PATCH, submit, approve officer stage, export CSV', async () => { + const createRes = await request(app) + .post(`/org-budgets/${orgId}/budgets`) + .send({ fiscalYear: '2026', templateKey: 'annual_club', title: 'FY26' }); + expect(createRes.status).toBe(201); + const budgetId = createRes.body.data._id; + + const lineItems = createRes.body.data.lineItems.map((li) => { + if (li.key === 'operating') return { ...li, amount: 500 }; + return li; + }); + const patchRes = await request(app).patch(`/org-budgets/${orgId}/budgets/${budgetId}`).send({ lineItems }); + expect(patchRes.status).toBe(200); + + const subRes = await request(app).post(`/org-budgets/${orgId}/budgets/${budgetId}/submit`).send(); + expect(subRes.status).toBe(200); + expect(subRes.body.data.status).toBe('in_review'); + + const apprRes = await request(app) + .put(`/org-budgets/${orgId}/budgets/${budgetId}/stages/officer/approve`) + .send(); + expect(apprRes.status).toBe(200); + expect(apprRes.body.data.status).toBe('in_review'); + expect(apprRes.body.data.workflow.currentStageIndex).toBe(1); + + const csvRes = await request(app).get(`/org-budgets/${orgId}/budgets/${budgetId}/export?format=csv`); + expect(csvRes.status).toBe(200); + expect(csvRes.headers['content-type']).toMatch(/csv/); + expect(csvRes.text).toContain('operating'); + }); + + test('POST create returns 409 when non-rejected budget exists for same FY + template', async () => { + const first = await request(app) + .post(`/org-budgets/${orgId}/budgets`) + .send({ fiscalYear: '2027', templateKey: 'annual_club' }); + expect(first.status).toBe(201); + const second = await request(app) + .post(`/org-budgets/${orgId}/budgets`) + .send({ fiscalYear: '2027', templateKey: 'annual_club' }); + expect(second.status).toBe(409); + }); + + test('PUT request-revision requires a message', async () => { + const createRes = await request(app) + .post(`/org-budgets/${orgId}/budgets`) + .send({ fiscalYear: '2028', templateKey: 'annual_club' }); + const budgetId = createRes.body.data._id; + const lineItems = createRes.body.data.lineItems.map((li) => + li.key === 'operating' ? { ...li, amount: 100 } : li + ); + await request(app).patch(`/org-budgets/${orgId}/budgets/${budgetId}`).send({ lineItems }); + await request(app).post(`/org-budgets/${orgId}/budgets/${budgetId}/submit`).send(); + const bad = await request(app) + .put(`/org-budgets/${orgId}/budgets/${budgetId}/stages/officer/request-revision`) + .send({ message: ' ' }); + expect(bad.status).toBe(400); + const good = await request(app) + .put(`/org-budgets/${orgId}/budgets/${budgetId}/stages/officer/request-revision`) + .send({ message: 'Add more detail to operating.' }); + expect(good.status).toBe(200); + expect(good.body.data.status).toBe('revision_requested'); + }); +}); diff --git a/backend/tests/route-outcomes/userRoutes.outcomes.test.js b/backend/tests/route-outcomes/userRoutes.outcomes.test.js index 1a348cf3..0a1cdb4c 100644 --- a/backend/tests/route-outcomes/userRoutes.outcomes.test.js +++ b/backend/tests/route-outcomes/userRoutes.outcomes.test.js @@ -107,4 +107,79 @@ describe('user route outcome tests', () => { expect(response.body.success).toBe(true); expect(response.body.message).toBe('Username is available'); }); + + test('POST /update-user rejects taken username with 400', async () => { + const alice = await new User({ + username: 'aliceupdate', + email: 'alice_up@example.com', + password: 'password123', + }).save(); + + await new User({ + username: 'bobupdate', + email: 'bob_up@example.com', + password: 'password123', + }).save(); + + const accessToken = jwt.sign( + {userId: alice._id.toString(), roles: ['user']}, + process.env.JWT_SECRET, + {expiresIn: '1h'}, + ); + + const response = await request(app) + .post('/update-user') + .set('Authorization', `Bearer ${accessToken}`) + .send({name: 'Alice', username: 'bobupdate'}); + + expect(response.statusCode).toBe(400); + expect(response.body.success).toBe(false); + expect(response.body.code).toBe('USERNAME_TAKEN'); + expect(response.body.field).toBe('username'); + }); + + test('POST /update-user allows keeping same username with different casing', async () => { + const alice = await new User({ + username: 'CaseUser', + email: 'case_up@example.com', + password: 'password123', + }).save(); + + const accessToken = jwt.sign( + {userId: alice._id.toString(), roles: ['user']}, + process.env.JWT_SECRET, + {expiresIn: '1h'}, + ); + + const response = await request(app) + .post('/update-user') + .set('Authorization', `Bearer ${accessToken}`) + .send({name: 'Alice Name', username: 'caseuser'}); + + expect(response.statusCode).toBe(200); + expect(response.body.success).toBe(true); + }); + + test('POST /update-user rejects invalid username pattern', async () => { + const alice = await new User({ + username: 'validuser99', + email: 'invalid_pat@example.com', + password: 'password123', + }).save(); + + const accessToken = jwt.sign( + {userId: alice._id.toString(), roles: ['user']}, + process.env.JWT_SECRET, + {expiresIn: '1h'}, + ); + + const response = await request(app) + .post('/update-user') + .set('Authorization', `Bearer ${accessToken}`) + .send({name: 'Alice', username: 'bad_name'}); + + expect(response.statusCode).toBe(400); + expect(response.body.code).toBe('USERNAME_INVALID'); + expect(response.body.field).toBe('username'); + }); }); diff --git a/backend/tests/unit/adminTenantEventsService.test.js b/backend/tests/unit/adminTenantEventsService.test.js new file mode 100644 index 00000000..d91fb917 --- /dev/null +++ b/backend/tests/unit/adminTenantEventsService.test.js @@ -0,0 +1,162 @@ +jest.mock('../../services/getModelService', () => jest.fn()); + +const getModels = require('../../services/getModelService'); +const { listAdminTenantUpcomingEvents } = require('../../services/adminTenantEventsService'); + +describe('adminTenantEventsService', () => { + it('returns paginated events and total', async () => { + const leanEvents = [{ _id: '1', name: 'A', status: 'approved' }]; + const mockFind = { + select: jest.fn().mockReturnThis(), + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + lean: jest.fn().mockResolvedValue(leanEvents), + }; + const aggregate = jest.fn().mockResolvedValue([]); + getModels.mockReturnValue({ + Event: { + find: jest.fn(() => mockFind), + countDocuments: jest.fn().mockResolvedValue(3), + }, + AnalyticsEvent: { + aggregate, + }, + }); + + const req = {}; + const out = await listAdminTenantUpcomingEvents(req, { page: 1, limit: 20 }); + + expect(out.events).toEqual([ + { + ...leanEvents[0], + analyticsSummary: { + views: 0, + uniqueViews: 0, + registrations: 0, + uniqueRegistrations: 0, + }, + }, + ]); + expect(out.pagination.total).toBe(3); + expect(out.pagination.page).toBe(1); + expect(out.pagination.limit).toBe(20); + expect(out.pagination.totalPages).toBe(1); + }); + + it('omits end_time filter when includePast is true and sorts by start_time desc', async () => { + const mockFind = { + select: jest.fn().mockReturnThis(), + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + lean: jest.fn().mockResolvedValue([]), + }; + const aggregate = jest.fn().mockResolvedValue([]); + const countDocuments = jest.fn().mockResolvedValue(0); + getModels.mockReturnValue({ + Event: { + find: jest.fn(() => mockFind), + countDocuments, + }, + AnalyticsEvent: { + aggregate, + }, + }); + + await listAdminTenantUpcomingEvents({}, { page: 1, limit: 20, includePast: true }); + + expect(countDocuments).toHaveBeenCalledWith( + expect.objectContaining({ + isDeleted: { $ne: true }, + }) + ); + const qArg = countDocuments.mock.calls[0][0]; + expect(qArg).not.toHaveProperty('end_time'); + expect(mockFind.sort).toHaveBeenCalledWith({ start_time: -1 }); + }); + + it('ignores q shorter than 3 characters', async () => { + const mockFind = { + select: jest.fn().mockReturnThis(), + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + lean: jest.fn().mockResolvedValue([]), + }; + const aggregate = jest.fn().mockResolvedValue([]); + const countDocuments = jest.fn().mockResolvedValue(0); + getModels.mockReturnValue({ + Event: { + find: jest.fn(() => mockFind), + countDocuments, + }, + AnalyticsEvent: { + aggregate, + }, + }); + + await listAdminTenantUpcomingEvents({}, { page: 1, limit: 20, q: 'ab' }); + + const qArg = countDocuments.mock.calls[0][0]; + expect(qArg).not.toHaveProperty('name'); + }); + + it('applies case-insensitive name regex when q has at least 3 characters', async () => { + const mockFind = { + select: jest.fn().mockReturnThis(), + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + lean: jest.fn().mockResolvedValue([]), + }; + const aggregate = jest.fn().mockResolvedValue([]); + const countDocuments = jest.fn().mockResolvedValue(0); + getModels.mockReturnValue({ + Event: { + find: jest.fn(() => mockFind), + countDocuments, + }, + AnalyticsEvent: { + aggregate, + }, + }); + + await listAdminTenantUpcomingEvents({}, { page: 1, limit: 20, q: 'Meet' }); + + expect(countDocuments).toHaveBeenCalledWith( + expect.objectContaining({ + name: { $regex: 'Meet', $options: 'i' }, + }) + ); + }); + + it('escapes regex special characters in q', async () => { + const mockFind = { + select: jest.fn().mockReturnThis(), + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + lean: jest.fn().mockResolvedValue([]), + }; + const aggregate = jest.fn().mockResolvedValue([]); + const countDocuments = jest.fn().mockResolvedValue(0); + getModels.mockReturnValue({ + Event: { + find: jest.fn(() => mockFind), + countDocuments, + }, + AnalyticsEvent: { + aggregate, + }, + }); + + await listAdminTenantUpcomingEvents({}, { page: 1, limit: 20, q: 'a+b' }); + + expect(countDocuments).toHaveBeenCalledWith( + expect.objectContaining({ + name: { $regex: 'a\\+b', $options: 'i' }, + }) + ); + }); +}); diff --git a/backend/tests/unit/adminTenantSummaryService.test.js b/backend/tests/unit/adminTenantSummaryService.test.js new file mode 100644 index 00000000..a6e7a2f1 --- /dev/null +++ b/backend/tests/unit/adminTenantSummaryService.test.js @@ -0,0 +1,26 @@ +jest.mock('../../services/getModelService', () => jest.fn()); + +const getModels = require('../../services/getModelService'); +const { getAdminTenantSummary } = require('../../services/adminTenantSummaryService'); + +describe('adminTenantSummaryService', () => { + it('returns counts from models', async () => { + getModels.mockReturnValue({ + Org: { countDocuments: jest.fn().mockResolvedValue(2) }, + Event: { countDocuments: jest.fn().mockResolvedValue(5) }, + Domain: { countDocuments: jest.fn().mockResolvedValue(1) }, + User: { countDocuments: jest.fn().mockResolvedValue(42) }, + }); + + const req = { db: {}, school: 'test' }; + const out = await getAdminTenantSummary(req); + + expect(out).toEqual({ + communityGroupCount: 2, + upcomingEventsCount: 5, + programsCount: 1, + userCount: 42, + }); + expect(getModels).toHaveBeenCalledWith(req, 'Org', 'Event', 'Domain', 'User'); + }); +}); diff --git a/backend/tests/unit/atlasPolicyService.test.js b/backend/tests/unit/atlasPolicyService.test.js new file mode 100644 index 00000000..c9b08e7f --- /dev/null +++ b/backend/tests/unit/atlasPolicyService.test.js @@ -0,0 +1,105 @@ +const { + assertLifecycleTransition, + governanceRequirementsForOrg, + assertOrgAllowsEventCreation, + assertEventReservationReady, + getReservationEscalation, + getEffectivePolicyFromConfig, + DEFAULT_ATLAS_POLICY +} = require('../../services/atlasPolicyService'); + +describe('atlasPolicyService', () => { + const org = (overrides = {}) => ({ + lifecycleStatus: 'active', + orgTypeKey: 'club', + ...overrides + }); + + test('getEffectivePolicyFromConfig merges defaults when atlasPolicy missing', () => { + const policy = getEffectivePolicyFromConfig({}); + expect(policy.lifecycle.defaultStatus).toBe(DEFAULT_ATLAS_POLICY.lifecycle.defaultStatus); + expect(policy.orgTypes.length).toBeGreaterThan(0); + }); + + test('assertLifecycleTransition allows valid transition for officer', () => { + const policy = getEffectivePolicyFromConfig({}); + expect(() => + assertLifecycleTransition(policy, org({ lifecycleStatus: 'active' }), 'sunset', { + isPlatformAdmin: false, + isOfficer: true + }) + ).not.toThrow(); + }); + + test('assertLifecycleTransition rejects invalid transition', () => { + const policy = getEffectivePolicyFromConfig({}); + expect(() => + assertLifecycleTransition(policy, org({ lifecycleStatus: 'active' }), 'inactive', { + isPlatformAdmin: false, + isOfficer: true + }) + ).toThrow(); + }); + + test('governanceRequirementsForOrg resolves org type', () => { + const policy = getEffectivePolicyFromConfig({ + atlasPolicy: { + orgTypes: [ + { key: 'club', requiredGovernanceKeys: ['constitution', 'member_list'] }, + { key: 'default', requiredGovernanceKeys: ['constitution'] } + ], + defaultOrgTypeKey: 'default' + } + }); + const keys = governanceRequirementsForOrg(policy, org({ orgTypeKey: 'club' })); + expect(keys).toContain('constitution'); + expect(keys).toContain('member_list'); + }); + + test('assertOrgAllowsEventCreation blocks sunset when configured', () => { + const policy = getEffectivePolicyFromConfig({}); + const r = assertOrgAllowsEventCreation(policy, org({ lifecycleStatus: 'sunset' })); + expect(r.ok).toBe(false); + }); + + test('assertOrgAllowsEventCreation allows active', () => { + const policy = getEffectivePolicyFromConfig({}); + const r = assertOrgAllowsEventCreation(policy, org({ lifecycleStatus: 'active' })); + expect(r.ok).toBe(true); + }); + + test('assertEventReservationReady blocks unresolved reservation conflicts', () => { + const r = assertEventReservationReady({ + classroom_id: '507f1f77bcf86cd799439011', + reservation: { + state: 'requested', + conflictSummary: { hasConflict: true, reason: 'Overlapping booking' } + } + }); + expect(r.ok).toBe(false); + expect(r.code).toBe('EVENT_RESERVATION_CONFLICT'); + }); + + test('getReservationEscalation flags old conflicts as high severity', () => { + const old = new Date(Date.now() - 30 * 60 * 60 * 1000); + const out = getReservationEscalation({ + reservation: { + conflictSummary: { hasConflict: true, reason: 'Overlapping booking' }, + detectedAt: old + } + }, { escalationThresholdHours: 24 }); + expect(out.escalated).toBe(true); + expect(out.severity).toBe('high'); + }); + + test('assertEventReservationReady allows approved reservation state', () => { + const r = assertEventReservationReady({ + classroom_id: '507f1f77bcf86cd799439011', + reservation: { + state: 'approved', + conflictSummary: { hasConflict: false, reason: '' } + } + }); + expect(r.ok).toBe(true); + }); +}); diff --git a/backend/tests/unit/budgetService.unit.test.js b/backend/tests/unit/budgetService.unit.test.js new file mode 100644 index 00000000..37c71eef --- /dev/null +++ b/backend/tests/unit/budgetService.unit.test.js @@ -0,0 +1,56 @@ +jest.mock('../../services/getModelService', () => jest.fn()); + +const { + toCsv, + materializeLineItems, + validateRequiredLineItems, + budgetToExportRows +} = require('../../services/budgetService'); + +describe('budgetService helpers', () => { + test('toCsv escapes quotes and commas', () => { + const rows = [ + ['a', 'b'], + ['hello,world', 'say "hi"'] + ]; + const csv = toCsv(rows); + expect(csv).toContain('"hello,world"'); + expect(csv).toContain('""'); + }); + + test('materializeLineItems maps incoming values by key', () => { + const template = { + lineItemDefinitions: [ + { key: 'x', label: 'X', required: true, kind: 'currency' }, + { key: 'y', label: 'Y', required: false, kind: 'text' } + ] + }; + const li = materializeLineItems(template, [{ key: 'x', amount: 100, note: 'n' }, { key: 'y', textValue: 'hello' }]); + expect(li).toHaveLength(2); + expect(li[0].amount).toBe(100); + expect(li[0].note).toBe('n'); + expect(li[1].textValue).toBe('hello'); + }); + + test('validateRequiredLineItems fails when currency missing', () => { + const template = { + lineItemDefinitions: [{ key: 'x', label: 'X', required: true, kind: 'currency' }] + }; + const li = [{ key: 'x', amount: null, label: 'X', kind: 'currency' }]; + const v = validateRequiredLineItems(template, li); + expect(v.ok).toBe(false); + }); + + test('budgetToExportRows includes line items', () => { + const budget = { + _id: '507f1f77bcf86cd799439011', + title: 'T', + fiscalYear: '2026', + templateKey: 'annual_club', + status: 'draft', + lineItems: [{ key: 'operating', label: 'Op', amount: 50, numberValue: null, textValue: '', note: '' }] + }; + const rows = budgetToExportRows(budget); + expect(rows.some((r) => r[0] === 'operating')).toBe(true); + }); +}); diff --git a/backend/tests/unit/reservationMetricsService.test.js b/backend/tests/unit/reservationMetricsService.test.js new file mode 100644 index 00000000..16159b6e --- /dev/null +++ b/backend/tests/unit/reservationMetricsService.test.js @@ -0,0 +1,33 @@ +jest.mock('../../services/getModelService', () => jest.fn()); + +const getModels = require('../../services/getModelService'); +const ReservationMetricsService = require('../../services/reservationMetricsService'); + +describe('reservationMetricsService', () => { + test('getMetrics returns summary and rate', async () => { + const Event = { + aggregate: jest + .fn() + .mockResolvedValueOnce([{ totalReservations: 10, conflicts: 2, unresolved: 1, approvedReservations: 5 }]) + .mockResolvedValueOnce([{ _id: 'r1', reservations: 4, conflicts: 1 }]) + }; + getModels.mockReturnValue({ Event }); + const svc = new ReservationMetricsService({ db: {} }); + const out = await svc.getMetrics({}); + expect(out.totalReservations).toBe(10); + expect(out.conflictRate).toBe(0.2); + expect(out.byResource.length).toBe(1); + }); + + test('toCsv serializes metric rows', () => { + const csv = ReservationMetricsService.toCsv({ + totalReservations: 3, + conflicts: 1, + unresolved: 1, + approvedReservations: 2, + conflictRate: 0.333 + }); + expect(csv).toMatch(/totalReservations,3/); + expect(csv).toMatch(/conflictRate,0.333/); + }); +}); diff --git a/backend/tests/unit/resourceReservationService.test.js b/backend/tests/unit/resourceReservationService.test.js new file mode 100644 index 00000000..b3a2cdf7 --- /dev/null +++ b/backend/tests/unit/resourceReservationService.test.js @@ -0,0 +1,107 @@ +jest.mock('../../services/getModelService', () => jest.fn()); + +const getModels = require('../../services/getModelService'); +const ResourceReservationService = require('../../services/resourceReservationService'); + +describe('resourceReservationService', () => { + const makeReq = () => ({ db: {} }); + + test('checkAvailability returns available when no resource linked', async () => { + getModels.mockReturnValue({ + Event: { find: jest.fn() }, + Schedule: { findOne: jest.fn() }, + Classroom: { findById: jest.fn() } + }); + const svc = new ResourceReservationService(makeReq()); + const out = await svc.checkAvailability({ + startTime: '2026-01-01T10:00:00.000Z', + endTime: '2026-01-01T11:00:00.000Z', + resourceId: null + }); + expect(out.isAvailable).toBe(true); + }); + + test('checkAvailability returns conflict when overlapping event exists', async () => { + const Event = { + find: jest.fn().mockReturnValue({ + select: jest.fn().mockResolvedValue([{ _id: 'x1' }]) + }) + }; + const Schedule = { findOne: jest.fn().mockResolvedValue(null) }; + getModels.mockReturnValue({ + Event, + Schedule, + Classroom: { findById: jest.fn() } + }); + const svc = new ResourceReservationService(makeReq()); + const out = await svc.checkAvailability({ + startTime: '2026-01-01T10:00:00.000Z', + endTime: '2026-01-01T11:00:00.000Z', + resourceId: '507f1f77bcf86cd799439011' + }); + expect(out.isAvailable).toBe(false); + expect(out.reason).toMatch(/existing event bookings/i); + expect(Event.find).toHaveBeenCalled(); + }); + + test('normalizeEventReservation maps legacy classroom_id to resourceId', () => { + getModels.mockReturnValue({ + Event: { find: jest.fn() }, + Schedule: { findOne: jest.fn() }, + Classroom: { findById: jest.fn() } + }); + const svc = new ResourceReservationService(makeReq()); + const out = svc.normalizeEventReservation({ + classroom_id: '507f1f77bcf86cd799439011', + status: 'pending' + }); + expect(String(out.resourceId)).toBe('507f1f77bcf86cd799439011'); + expect(out.state).toBe('requested'); + }); + + test('applyAvailabilitySnapshot marks unresolved exception fields on conflict', async () => { + const Event = { + find: jest.fn().mockReturnValue({ + select: jest.fn().mockResolvedValue([{ _id: 'x1' }]) + }) + }; + const Schedule = { findOne: jest.fn().mockResolvedValue(null) }; + getModels.mockReturnValue({ + Event, + Schedule, + Classroom: { findById: jest.fn() } + }); + const svc = new ResourceReservationService(makeReq()); + const eventDoc = { status: 'approved', classroom_id: '507f1f77bcf86cd799439011' }; + await svc.applyAvailabilitySnapshot(eventDoc, { + startTime: '2026-01-01T10:00:00.000Z', + endTime: '2026-01-01T11:00:00.000Z', + resourceId: '507f1f77bcf86cd799439011' + }); + expect(eventDoc.reservation.conflictSummary.hasConflict).toBe(true); + expect(eventDoc.reservation.resolutionStatus).toBe('unresolved'); + expect(eventDoc.reservation.detectedAt).toBeTruthy(); + }); + + test('applyExceptionState resolves conflict and appends history', () => { + getModels.mockReturnValue({ + Event: { find: jest.fn() }, + Schedule: { findOne: jest.fn() }, + Classroom: { findById: jest.fn() } + }); + const svc = new ResourceReservationService(makeReq()); + const eventDoc = { + reservation: { + resourceId: '507f1f77bcf86cd799439011', + state: 'requested', + conflictSummary: { hasConflict: true, reason: 'Overlapping booking' }, + resolutionStatus: 'unresolved', + history: [] + } + }; + svc.applyExceptionState(eventDoc, { action: 'resolved', note: 'Adjusted schedule', actorId: 'u1' }); + expect(eventDoc.reservation.conflictSummary.hasConflict).toBe(false); + expect(eventDoc.reservation.resolutionStatus).toBe('resolved'); + expect(eventDoc.reservation.history.length).toBeGreaterThan(0); + }); +}); diff --git a/backend/tests/unit/rootOperatorUsersService.test.js b/backend/tests/unit/rootOperatorUsersService.test.js new file mode 100644 index 00000000..801863c3 --- /dev/null +++ b/backend/tests/unit/rootOperatorUsersService.test.js @@ -0,0 +1,65 @@ +jest.mock('../../services/getModelService', () => jest.fn()); + +const getModels = require('../../services/getModelService'); +const { + getRootOperatorUserStats, + searchRootOperatorUsers, + setRootOperatorUserRole, + setRootOperatorAccessSuspended, +} = require('../../services/rootOperatorUsersService'); + +describe('rootOperatorUsersService', () => { + it('getRootOperatorUserStats returns admin and member counts', async () => { + getModels.mockReturnValue({ + User: { + countDocuments: jest.fn().mockResolvedValueOnce(100).mockResolvedValueOnce(7), + }, + }); + const out = await getRootOperatorUserStats({}); + expect(out).toEqual({ + totalUsers: 100, + adminCount: 7, + memberCount: 93, + }); + }); + + it('searchRootOperatorUsers returns empty when query is short', async () => { + getModels.mockReturnValue({ User: {} }); + const out = await searchRootOperatorUsers({}, { q: 'a' }); + expect(out).toEqual({ users: [], total: 0 }); + }); + + it('setRootOperatorUserRole adds admin', async () => { + const target = { roles: ['user'], save: jest.fn().mockResolvedValue(true) }; + const actor = { roles: ['admin'] }; + const findById = jest.fn((id) => { + if (String(id) === 'actor') return Promise.resolve(actor); + if (String(id) === 'target') return Promise.resolve(target); + return Promise.resolve(null); + }); + getModels.mockReturnValue({ + User: { + findById, + countDocuments: jest.fn().mockResolvedValue(2), + }, + }); + + const req = { user: { userId: 'actor' } }; + const out = await setRootOperatorUserRole(req, { + userId: 'target', + role: 'admin', + assign: true, + }); + + expect(out.roles).toContain('admin'); + expect(target.save).toHaveBeenCalled(); + }); + + it('setRootOperatorAccessSuspended rejects self', async () => { + getModels.mockReturnValue({ User: {} }); + const req = { user: { userId: 'same' } }; + await expect( + setRootOperatorAccessSuspended(req, { userId: 'same', accessSuspended: true }) + ).rejects.toMatchObject({ statusCode: 400 }); + }); +}); diff --git a/backend/tests/unit/workflowUtilities.test.js b/backend/tests/unit/workflowUtilities.test.js new file mode 100644 index 00000000..ff5a1394 --- /dev/null +++ b/backend/tests/unit/workflowUtilities.test.js @@ -0,0 +1,87 @@ +jest.mock('../../events/backendRoot', () => ({ + require: jest.fn(() => jest.fn()) +})); + +const { + getDomainSpaceGovernance, + scopeMatchesEventContext +} = require('../../events/utilities/workflowUtilities'); + +describe('workflowUtilities governance helpers', () => { + test('getDomainSpaceGovernance returns defaults', () => { + const governance = getDomainSpaceGovernance({}); + expect(governance.governingScope.kind).toBe('all_spaces'); + expect(governance.concernScope.kind).toBe('campus_wide'); + expect(governance.scopeMode).toBe('inclusive'); + }); + + test('scopeMatchesEventContext supports all_spaces and campus_wide', () => { + const eventSpaceContext = { resourceId: '', building: '', isCampusWide: true }; + expect(scopeMatchesEventContext({ kind: 'all_spaces' }, eventSpaceContext)).toBe(true); + expect(scopeMatchesEventContext({ kind: 'campus_wide' }, eventSpaceContext)).toBe(true); + }); + + test('scopeMatchesEventContext matches scoped building and space', () => { + const ctx = { + resourceId: '507f1f77bcf86cd799439011', + building: 'Union', + isCampusWide: false + }; + expect( + scopeMatchesEventContext( + { kind: 'scoped', buildingIds: ['union'], spaceIds: [], spaceGroupIds: [] }, + ctx + ) + ).toBe(true); + expect( + scopeMatchesEventContext( + { kind: 'scoped', buildingIds: [], spaceIds: ['507f1f77bcf86cd799439011'], spaceGroupIds: [] }, + ctx + ) + ).toBe(true); + }); + + test('scopeMatchesEventContext matches scoped building by building ObjectId', () => { + const bid = '507f1f77bcf86cd799439011'; + const ctx = { + resourceId: 'aaaaaaaaaaaaaaaaaaaaaaaa', + building: 'Some Hall', + buildingId: bid, + isCampusWide: false + }; + expect( + scopeMatchesEventContext( + { kind: 'scoped', buildingIds: [bid], spaceIds: [], spaceGroupIds: [] }, + ctx + ) + ).toBe(true); + expect( + scopeMatchesEventContext( + { kind: 'scoped', buildingIds: ['aaaaaaaaaaaaaaaaaaaaaaaa'], spaceIds: [], spaceGroupIds: [] }, + ctx + ) + ).toBe(false); + }); + + test('scopeMatchesEventContext respects exclusive mode', () => { + const ctx = { + resourceId: '507f1f77bcf86cd799439011', + building: 'Union', + isCampusWide: false + }; + expect( + scopeMatchesEventContext( + { kind: 'scoped', buildingIds: ['union'], spaceIds: ['507f1f77bcf86cd799439011'], spaceGroupIds: [] }, + ctx, + 'exclusive' + ) + ).toBe(true); + expect( + scopeMatchesEventContext( + { kind: 'scoped', buildingIds: ['library'], spaceIds: ['507f1f77bcf86cd799439011'], spaceGroupIds: [] }, + ctx, + 'exclusive' + ) + ).toBe(false); + }); +}); diff --git a/backend/utilities/classroomBuildingName.js b/backend/utilities/classroomBuildingName.js new file mode 100644 index 00000000..334ff08b --- /dev/null +++ b/backend/utilities/classroomBuildingName.js @@ -0,0 +1,13 @@ +/** + * Display string for a classroom's building (populated { name } or legacy string). + */ +function classroomBuildingName(room) { + if (!room || room.building == null || room.building === '') return ''; + const b = room.building; + if (typeof b === 'object' && b !== null && 'name' in b) { + return String(b.name || ''); + } + return String(b); +} + +module.exports = { classroomBuildingName }; diff --git a/backend/utilities/workflowUtilities.js b/backend/utilities/workflowUtilities.js index 41ec8013..cf2ced31 100644 --- a/backend/utilities/workflowUtilities.js +++ b/backend/utilities/workflowUtilities.js @@ -1,24 +1,47 @@ const getModels = require('../services/getModelService'); +function normalizeComparable(value) { + if (typeof value === 'string') return value.trim().toLowerCase(); + return value; +} + +function asComparableArray(value) { + if (Array.isArray(value)) return value.map(normalizeComparable); + if (typeof value === 'string') return value.split(',').map((v) => normalizeComparable(v)).filter(Boolean); + return [normalizeComparable(value)]; +} + // Helper function to evaluate a single condition function evaluateCondition(condition, event) { - const value = event[condition.field]; + const fieldAliases = { + room: 'location', + roomName: 'location', + classroom: 'classroom_id', + resourceId: 'classroom_id', + eventType: 'type', + startTime: 'start_time', + endTime: 'end_time' + }; + const fieldKey = fieldAliases[condition.field] || condition.field; + const value = event[fieldKey]; if (value === undefined) return false; + const normalizedValue = normalizeComparable(value); + const normalizedConditionValue = normalizeComparable(condition.value); switch (condition.operator) { // String operators case 'equals': - return value === condition.value; + return normalizedValue === normalizedConditionValue; case 'notEquals': - return value !== condition.value; + return normalizedValue !== normalizedConditionValue; case 'contains': - return value.includes(condition.value); + return String(normalizedValue).includes(String(normalizedConditionValue)); case 'notContains': - return !value.includes(condition.value); + return !String(normalizedValue).includes(String(normalizedConditionValue)); case 'in': - return condition.value.includes(value); + return asComparableArray(condition.value).includes(normalizedValue); case 'notIn': - return !condition.value.includes(value); + return !asComparableArray(condition.value).includes(normalizedValue); // Number operators case 'greaterThan': diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a7f79701..fac6147a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,6 +19,9 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "@tiptap/extension-placeholder": "^3.22.2", + "@tiptap/react": "^3.22.2", + "@tiptap/starter-kit": "^3.22.2", "@visx/xychart": "^3.12.0", "@xyflow/react": "^12.9.2", "axios": "^1.6.5", @@ -2520,6 +2523,34 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT", + "optional": true + }, "node_modules/@gilbarbara/deep-equal": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@gilbarbara/deep-equal/-/deep-equal-0.3.1.tgz", @@ -2674,16 +2705,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@isaacs/ttlcache": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", - "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=12" - } - }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -2953,143 +2974,6 @@ "node": ">=8" } }, - "node_modules/@jest/create-cache-key-function": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", - "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@jest/types": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/create-cache-key-function/node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/create-cache-key-function/node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/create-cache-key-function/node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "license": "MIT", - "peer": true - }, - "node_modules/@jest/create-cache-key-function/node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@jest/create-cache-key-function/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "peer": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/create-cache-key-function/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/create-cache-key-function/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/create-cache-key-function/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true - }, - "node_modules/@jest/create-cache-key-function/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/create-cache-key-function/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "peer": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/environment": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-27.5.1.tgz", @@ -3962,1206 +3846,1166 @@ } } }, - "node_modules/@react-native/assets-registry": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.84.1.tgz", - "integrity": "sha512-lAJ6PDZv95FdT9s9uhc9ivhikW1Zwh4j9XdXM7J2l4oUA3t37qfoBmTSDLuPyE3Bi+Xtwa11hJm0BUTT2sc/gg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 20.19.4" + "node_modules/@react-oauth/google": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.1.tgz", + "integrity": "sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, - "node_modules/@react-native/codegen": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.84.1.tgz", - "integrity": "sha512-n1RIU0QAavgCg1uC5+s53arL7/mpM+16IBhJ3nCFSd/iK5tUmCwxQDcIDC703fuXfpub/ZygeSjVN8bcOWn0gA==", - "license": "MIT", - "peer": true, + "node_modules/@react-spring/animated": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.3.tgz", + "integrity": "sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==", "dependencies": { - "@babel/core": "^7.25.2", - "@babel/parser": "^7.25.3", - "hermes-parser": "0.32.0", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "tinyglobby": "^0.2.15", - "yargs": "^17.6.2" - }, - "engines": { - "node": ">= 20.19.4" + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" }, "peerDependencies": { - "@babel/core": "*" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/@react-native/codegen/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "peer": true, + "node_modules/@react-spring/core": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.3.tgz", + "integrity": "sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ==", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "@react-spring/animated": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" }, - "engines": { - "node": ">=12" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/@react-native/codegen/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "peer": true, + "node_modules/@react-spring/konva": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/konva/-/konva-9.7.3.tgz", + "integrity": "sha512-R9sY6SiPGYqz1383P5qppg5z57YfChVknOC1UxxaGxpw+WiZa8fZ4zmZobslrw+os3/+HAXZv8O+EvU/nQpf7g==", "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "@react-spring/animated": "~9.7.3", + "@react-spring/core": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" }, - "engines": { - "node": ">=12" + "peerDependencies": { + "konva": ">=2.6", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-konva": "^16.8.0 || ^16.8.7-0 || ^16.9.0-0 || ^16.10.1-0 || ^16.12.0-0 || ^16.13.0-0 || ^17.0.0-0 || ^17.0.1-0 || ^17.0.2-0 || ^18.0.0-0" } }, - "node_modules/@react-native/codegen/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "peer": true, - "engines": { - "node": ">=12" + "node_modules/@react-spring/native": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/native/-/native-9.7.3.tgz", + "integrity": "sha512-4mpxX3FuEBCUT6ae2fjhxcJW6bhr2FBwFf274eXB7n+U30Gdg8Wo2qYwcUnmiAA0S3dvP8vLTazx3+CYWFShnA==", + "dependencies": { + "@react-spring/animated": "~9.7.3", + "@react-spring/core": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" + }, + "peerDependencies": { + "react": "^16.8.0 || >=17.0.0 || >=18.0.0", + "react-native": ">=0.58" } }, - "node_modules/@react-native/community-cli-plugin": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.84.1.tgz", - "integrity": "sha512-f6a+mJEJ6Joxlt/050TqYUr7uRRbeKnz8lnpL7JajhpsgZLEbkJRjH8HY5QiLcRdUwWFtizml4V+vcO3P4RxoQ==", - "license": "MIT", - "peer": true, + "node_modules/@react-spring/shared": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.3.tgz", + "integrity": "sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA==", "dependencies": { - "@react-native/dev-middleware": "0.84.1", - "debug": "^4.4.0", - "invariant": "^2.2.4", - "metro": "^0.83.3", - "metro-config": "^0.83.3", - "metro-core": "^0.83.3", - "semver": "^7.1.3" - }, - "engines": { - "node": ">= 20.19.4" + "@react-spring/types": "~9.7.3" }, "peerDependencies": { - "@react-native-community/cli": "*", - "@react-native/metro-config": "*" - }, - "peerDependenciesMeta": { - "@react-native-community/cli": { - "optional": true - }, - "@react-native/metro-config": { - "optional": true - } + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/@react-native/community-cli-plugin/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "peer": true, + "node_modules/@react-spring/three": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.7.3.tgz", + "integrity": "sha512-Q1p512CqUlmMK8UMBF/Rj79qndhOWq4XUTayxMP9S892jiXzWQuj+xC3Xvm59DP/D4JXusXpxxqfgoH+hmOktA==", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" + "@react-spring/animated": "~9.7.3", + "@react-spring/core": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "peerDependencies": { + "@react-three/fiber": ">=6.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "three": ">=0.126" } }, - "node_modules/@react-native/community-cli-plugin/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT", - "peer": true - }, - "node_modules/@react-native/debugger-frontend": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.84.1.tgz", - "integrity": "sha512-rUU/Pyh3R5zT0WkVgB+yA6VwOp7HM5Hz4NYE97ajFS07OUIcv8JzBL3MXVdSSjLfldfqOuPEuKUaZcAOwPgabw==", - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": ">= 20.19.4" - } + "node_modules/@react-spring/types": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.3.tgz", + "integrity": "sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw==" }, - "node_modules/@react-native/debugger-shell": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@react-native/debugger-shell/-/debugger-shell-0.84.1.tgz", - "integrity": "sha512-LIGhh4q4ette3yW5OzmukNMYwmINYrRGDZqKyTYc/VZyNpblZPw72coXVHXdfpPT6+YlxHqXzn3UjFZpNODGCQ==", - "license": "MIT", - "peer": true, + "node_modules/@react-spring/web": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.3.tgz", + "integrity": "sha512-BXt6BpS9aJL/QdVqEIX9YoUy8CE6TJrU0mNCqSoxdXlIeNcEBWOfIyE6B14ENNsyQKS3wOWkiJfco0tCr/9tUg==", "dependencies": { - "cross-spawn": "^7.0.6", - "debug": "^4.4.0", - "fb-dotslash": "0.5.8" + "@react-spring/animated": "~9.7.3", + "@react-spring/core": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" }, - "engines": { - "node": ">= 20.19.4" + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/@react-native/debugger-shell/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "peer": true, + "node_modules/@react-spring/zdog": { + "version": "9.7.3", + "resolved": "https://registry.npmjs.org/@react-spring/zdog/-/zdog-9.7.3.tgz", + "integrity": "sha512-L+yK/1PvNi9n8cldiJ309k4LdxcPkeWE0W18l1zrP1IBIyd5NB5EPA8DMsGr9gtNnnIujtEzZk+4JIOjT8u/tw==", "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" + "@react-spring/animated": "~9.7.3", + "@react-spring/core": "~9.7.3", + "@react-spring/shared": "~9.7.3", + "@react-spring/types": "~9.7.3" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-zdog": ">=1.0", + "zdog": ">=1.0" } }, - "node_modules/@react-native/debugger-shell/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT", - "peer": true + "node_modules/@remirror/core-constants": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", + "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==", + "license": "MIT" }, - "node_modules/@react-native/dev-middleware": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.84.1.tgz", - "integrity": "sha512-Z83ra+Gk6ElAhH3XRrv3vwbwCPTb04sPPlNpotxcFZb5LtRQZwT91ZQEXw3GOJCVIFp9EQ/gj8AQbVvtHKOUlQ==", + "node_modules/@remix-run/router": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", + "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", "license": "MIT", - "peer": true, - "dependencies": { - "@isaacs/ttlcache": "^1.4.1", - "@react-native/debugger-frontend": "0.84.1", - "@react-native/debugger-shell": "0.84.1", - "chrome-launcher": "^0.15.2", - "chromium-edge-launcher": "^0.2.0", - "connect": "^3.6.5", - "debug": "^4.4.0", - "invariant": "^2.2.4", - "nullthrows": "^1.1.1", - "open": "^7.0.3", - "serve-static": "^1.16.2", - "ws": "^7.5.10" - }, "engines": { - "node": ">= 20.19.4" + "node": ">=14.0.0" } }, - "node_modules/@react-native/dev-middleware/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "peer": true, + "node_modules/@rollup/plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", + "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", "dependencies": { - "ms": "^2.1.3" + "@babel/helper-module-imports": "^7.10.4", + "@rollup/pluginutils": "^3.1.0" }, "engines": { - "node": ">=6.0" + "node": ">= 10.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0" }, "peerDependenciesMeta": { - "supports-color": { + "@types/babel__core": { "optional": true } } }, - "node_modules/@react-native/dev-middleware/node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@react-native/dev-middleware/node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "peer": true, + "node_modules/@rollup/plugin-node-resolve": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", + "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "builtin-modules": "^3.1.0", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.19.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 10.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" } }, - "node_modules/@react-native/dev-middleware/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT", - "peer": true + "node_modules/@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } }, - "node_modules/@react-native/dev-middleware/node_modules/open": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", - "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", - "license": "MIT", - "peer": true, + "node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", "dependencies": { - "is-docker": "^2.0.0", - "is-wsl": "^2.1.1" + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" }, "engines": { - "node": ">=8" + "node": ">= 8.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" } }, - "node_modules/@react-native/dev-middleware/node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "node_modules/@rollup/pluginutils/node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" + }, + "node_modules/@rsuite/icon-font": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@rsuite/icon-font/-/icon-font-4.1.0.tgz", + "integrity": "sha512-q0Y+uQCVvzhD6lFeAFrvCDd1lTjZfM6MIaBjre3lSW1w586VWbuFnhTiqos3v9HIMlUpm3aAsxd3SuM6gYaqqQ==", + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)" + }, + "node_modules/@rsuite/icons": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rsuite/icons/-/icons-1.3.0.tgz", + "integrity": "sha512-6yv2CQjtQGgSCkMw1wlVlmPFEKBTU9AFFFxPJbAI2V4kS9lZEHqhY+jmVSAdbC7rmawO5r2ROzGMJpvkpRCnUw==", "license": "MIT", - "peer": true, "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" + "@rsuite/icon-font": "^4.1.0", + "classnames": "^2.2.5" }, - "engines": { - "node": ">= 0.8.0" + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, - "node_modules/@react-native/dev-middleware/node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "peer": true, + "node_modules/@rushstack/eslint-patch": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.1.tgz", + "integrity": "sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==" + }, + "node_modules/@simplewebauthn/browser": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.3.0.tgz", + "integrity": "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==", + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.24.51", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", + "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==" + }, + "node_modules/@sinonjs/commons": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", "dependencies": { - "ms": "2.0.0" + "type-detect": "4.0.8" } }, - "node_modules/@react-native/dev-middleware/node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT", - "peer": true + "node_modules/@sinonjs/fake-timers": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", + "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" }, - "node_modules/@react-native/dev-middleware/node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "node_modules/@squircle-js/react": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@squircle-js/react/-/react-1.2.17.tgz", + "integrity": "sha512-ejCDGx6P+dNt0GwIcv22N/A17++jd3RvRY38EAf8q3vpvFmv0oN8snYVzHI5X/G5+QqwSlF6eRXT57+7FKn+jA==", "license": "MIT", - "peer": true, "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" + "@radix-ui/react-slot": "^1.1.2", + "figma-squircle": "1.1.0" }, - "engines": { - "node": ">= 0.8.0" + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19" } }, - "node_modules/@react-native/dev-middleware/node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" + "node_modules/@surma/rollup-plugin-off-main-thread": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", + "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", + "dependencies": { + "ejs": "^3.1.6", + "json5": "^2.2.0", + "magic-string": "^0.25.0", + "string.prototype.matchall": "^4.0.6" } }, - "node_modules/@react-native/gradle-plugin": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.84.1.tgz", - "integrity": "sha512-7uVlPBE3uluRNRX4MW7PUJIO1LDBTpAqStKHU7LHH+GRrdZbHsWtOEAX8PiY4GFfBEvG8hEjiuTOqAxMjV+hDg==", - "license": "MIT", - "peer": true, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz", + "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==", "engines": { - "node": ">= 20.19.4" + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@react-native/js-polyfills": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.84.1.tgz", - "integrity": "sha512-UsTe2AbUugsfyI7XIHMQq4E7xeC8a6GrYwuK+NohMMMJMxmyM3JkzIk+GB9e2il6ScEQNMJNaj+q+i5za8itxQ==", - "license": "MIT", - "peer": true, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz", + "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==", "engines": { - "node": ">= 20.19.4" + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@react-native/normalize-colors": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.84.1.tgz", - "integrity": "sha512-/UPaQ4jl95soXnLDEJ6Cs6lnRXhwbxtT4KbZz+AFDees7prMV2NOLcHfCnzmTabf5Y3oxENMVBL666n4GMLcTA==", - "license": "MIT", - "peer": true - }, - "node_modules/@react-oauth/google": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.1.tgz", - "integrity": "sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==", - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz", + "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@react-spring/animated": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.3.tgz", - "integrity": "sha512-5CWeNJt9pNgyvuSzQH+uy2pvTg8Y4/OisoscZIR8/ZNLIOI+CatFBhGZpDGTF/OzdNFsAoGk3wiUYTwoJ0YIvw==", - "dependencies": { - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz", + "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==", + "engines": { + "node": ">=10" }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@react-spring/core": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.3.tgz", - "integrity": "sha512-IqFdPVf3ZOC1Cx7+M0cXf4odNLxDC+n7IN3MDcVCTIOSBfqEcBebSv+vlY5AhM0zw05PDbjKrNmBpzv/AqpjnQ==", - "dependencies": { - "@react-spring/animated": "~9.7.3", - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz", + "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==", + "engines": { + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-spring/donate" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@react-spring/konva": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/konva/-/konva-9.7.3.tgz", - "integrity": "sha512-R9sY6SiPGYqz1383P5qppg5z57YfChVknOC1UxxaGxpw+WiZa8fZ4zmZobslrw+os3/+HAXZv8O+EvU/nQpf7g==", - "dependencies": { - "@react-spring/animated": "~9.7.3", - "@react-spring/core": "~9.7.3", - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz", + "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==", + "engines": { + "node": ">=10" }, - "peerDependencies": { - "konva": ">=2.6", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-konva": "^16.8.0 || ^16.8.7-0 || ^16.9.0-0 || ^16.10.1-0 || ^16.12.0-0 || ^16.13.0-0 || ^17.0.0-0 || ^17.0.1-0 || ^17.0.2-0 || ^18.0.0-0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@react-spring/native": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/native/-/native-9.7.3.tgz", - "integrity": "sha512-4mpxX3FuEBCUT6ae2fjhxcJW6bhr2FBwFf274eXB7n+U30Gdg8Wo2qYwcUnmiAA0S3dvP8vLTazx3+CYWFShnA==", - "dependencies": { - "@react-spring/animated": "~9.7.3", - "@react-spring/core": "~9.7.3", - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz", + "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==", + "engines": { + "node": ">=10" }, - "peerDependencies": { - "react": "^16.8.0 || >=17.0.0 || >=18.0.0", - "react-native": ">=0.58" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@react-spring/shared": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.3.tgz", - "integrity": "sha512-NEopD+9S5xYyQ0pGtioacLhL2luflh6HACSSDUZOwLHoxA5eku1UPuqcJqjwSD6luKjjLfiLOspxo43FUHKKSA==", - "dependencies": { - "@react-spring/types": "~9.7.3" + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz", + "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==", + "engines": { + "node": ">=10" }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@react-spring/three": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/three/-/three-9.7.3.tgz", - "integrity": "sha512-Q1p512CqUlmMK8UMBF/Rj79qndhOWq4XUTayxMP9S892jiXzWQuj+xC3Xvm59DP/D4JXusXpxxqfgoH+hmOktA==", + "node_modules/@svgr/babel-preset": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz", + "integrity": "sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==", "dependencies": { - "@react-spring/animated": "~9.7.3", - "@react-spring/core": "~9.7.3", - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" + "@svgr/babel-plugin-add-jsx-attribute": "^5.4.0", + "@svgr/babel-plugin-remove-jsx-attribute": "^5.4.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "^5.0.1", + "@svgr/babel-plugin-replace-jsx-attribute-value": "^5.0.1", + "@svgr/babel-plugin-svg-dynamic-title": "^5.4.0", + "@svgr/babel-plugin-svg-em-dimensions": "^5.4.0", + "@svgr/babel-plugin-transform-react-native-svg": "^5.4.0", + "@svgr/babel-plugin-transform-svg-component": "^5.5.0" }, - "peerDependencies": { - "@react-three/fiber": ">=6.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "three": ">=0.126" - } - }, - "node_modules/@react-spring/types": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.3.tgz", - "integrity": "sha512-Kpx/fQ/ZFX31OtlqVEFfgaD1ACzul4NksrvIgYfIFq9JpDHFwQkMVZ10tbo0FU/grje4rcL4EIrjekl3kYwgWw==" - }, - "node_modules/@react-spring/web": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.3.tgz", - "integrity": "sha512-BXt6BpS9aJL/QdVqEIX9YoUy8CE6TJrU0mNCqSoxdXlIeNcEBWOfIyE6B14ENNsyQKS3wOWkiJfco0tCr/9tUg==", - "dependencies": { - "@react-spring/animated": "~9.7.3", - "@react-spring/core": "~9.7.3", - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" + "engines": { + "node": ">=10" }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@react-spring/zdog": { - "version": "9.7.3", - "resolved": "https://registry.npmjs.org/@react-spring/zdog/-/zdog-9.7.3.tgz", - "integrity": "sha512-L+yK/1PvNi9n8cldiJ309k4LdxcPkeWE0W18l1zrP1IBIyd5NB5EPA8DMsGr9gtNnnIujtEzZk+4JIOjT8u/tw==", + "node_modules/@svgr/core": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz", + "integrity": "sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==", "dependencies": { - "@react-spring/animated": "~9.7.3", - "@react-spring/core": "~9.7.3", - "@react-spring/shared": "~9.7.3", - "@react-spring/types": "~9.7.3" + "@svgr/plugin-jsx": "^5.5.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^7.0.0" }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-zdog": ">=1.0", - "zdog": ">=1.0" - } - }, - "node_modules/@react-three/fiber": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.5.0.tgz", - "integrity": "sha512-FiUzfYW4wB1+PpmsE47UM+mCads7j2+giRBltfwH7SNhah95rqJs3ltEs9V3pP8rYdS0QlNne+9Aj8dS/SiaIA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/runtime": "^7.17.8", - "@types/webxr": "*", - "base64-js": "^1.5.1", - "buffer": "^6.0.3", - "its-fine": "^2.0.0", - "react-use-measure": "^2.1.7", - "scheduler": "^0.27.0", - "suspend-react": "^0.1.3", - "use-sync-external-store": "^1.4.0", - "zustand": "^5.0.3" - }, - "peerDependencies": { - "expo": ">=43.0", - "expo-asset": ">=8.4", - "expo-file-system": ">=11.0", - "expo-gl": ">=11.0", - "react": ">=19 <19.3", - "react-dom": ">=19 <19.3", - "react-native": ">=0.78", - "three": ">=0.156" + "engines": { + "node": ">=10" }, - "peerDependenciesMeta": { - "expo": { - "optional": true - }, - "expo-asset": { - "optional": true - }, - "expo-file-system": { - "optional": true - }, - "expo-gl": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@react-three/fiber/node_modules/its-fine": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz", - "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==", - "license": "MIT", - "peer": true, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz", + "integrity": "sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==", "dependencies": { - "@types/react-reconciler": "^0.28.9" + "@babel/types": "^7.12.6" }, - "peerDependencies": { - "react": "^19.0.0" - } - }, - "node_modules/@react-three/fiber/node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT", - "peer": true - }, - "node_modules/@react-three/fiber/node_modules/zustand": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", - "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", - "license": "MIT", - "peer": true, "engines": { - "node": ">=12.20.0" - }, - "peerDependencies": { - "@types/react": ">=18.0.0", - "immer": ">=9.0.6", - "react": ">=18.0.0", - "use-sync-external-store": ">=1.2.0" + "node": ">=10" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - }, - "use-sync-external-store": { - "optional": true - } - } - }, - "node_modules/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@rollup/plugin-babel": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", - "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", + "node_modules/@svgr/plugin-jsx": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz", + "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==", "dependencies": { - "@babel/helper-module-imports": "^7.10.4", - "@rollup/pluginutils": "^3.1.0" + "@babel/core": "^7.12.3", + "@svgr/babel-preset": "^5.5.0", + "@svgr/hast-util-to-babel-ast": "^5.5.0", + "svg-parser": "^2.0.2" }, "engines": { - "node": ">= 10.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "@types/babel__core": "^7.1.9", - "rollup": "^1.20.0||^2.0.0" + "node": ">=10" }, - "peerDependenciesMeta": { - "@types/babel__core": { - "optional": true - } + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "11.2.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz", - "integrity": "sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg==", + "node_modules/@svgr/plugin-svgo": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz", + "integrity": "sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==", "dependencies": { - "@rollup/pluginutils": "^3.1.0", - "@types/resolve": "1.17.1", - "builtin-modules": "^3.1.0", + "cosmiconfig": "^7.0.0", "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.19.0" + "svgo": "^1.2.2" }, "engines": { - "node": ">= 10.0.0" + "node": ">=10" }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@rollup/plugin-replace": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", - "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "node_modules/@svgr/webpack": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz", + "integrity": "sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==", "dependencies": { - "@rollup/pluginutils": "^3.1.0", - "magic-string": "^0.25.7" + "@babel/core": "^7.12.3", + "@babel/plugin-transform-react-constant-elements": "^7.12.1", + "@babel/preset-env": "^7.12.1", + "@babel/preset-react": "^7.12.5", + "@svgr/core": "^5.5.0", + "@svgr/plugin-jsx": "^5.5.0", + "@svgr/plugin-svgo": "^5.5.0", + "loader-utils": "^2.0.0" }, - "peerDependencies": { - "rollup": "^1.20.0 || ^2.0.0" + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@rollup/pluginutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", - "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "node_modules/@testing-library/jest-dom": { + "version": "5.17.0", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", + "integrity": "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==", "dependencies": { - "@types/estree": "0.0.39", - "estree-walker": "^1.0.1", - "picomatch": "^2.2.2" + "@adobe/css-tools": "^4.0.1", + "@babel/runtime": "^7.9.2", + "@types/testing-library__jest-dom": "^5.9.1", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.5.6", + "lodash": "^4.17.15", + "redent": "^3.0.0" }, "engines": { - "node": ">= 8.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0" + "node": ">=8", + "npm": ">=6", + "yarn": ">=1" } }, - "node_modules/@rollup/pluginutils/node_modules/@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" - }, - "node_modules/@rsuite/icon-font": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@rsuite/icon-font/-/icon-font-4.1.0.tgz", - "integrity": "sha512-q0Y+uQCVvzhD6lFeAFrvCDd1lTjZfM6MIaBjre3lSW1w586VWbuFnhTiqos3v9HIMlUpm3aAsxd3SuM6gYaqqQ==", - "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)" - }, - "node_modules/@rsuite/icons": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@rsuite/icons/-/icons-1.3.0.tgz", - "integrity": "sha512-6yv2CQjtQGgSCkMw1wlVlmPFEKBTU9AFFFxPJbAI2V4kS9lZEHqhY+jmVSAdbC7rmawO5r2ROzGMJpvkpRCnUw==", - "license": "MIT", + "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dependencies": { - "@rsuite/icon-font": "^4.1.0", - "classnames": "^2.2.5" + "color-convert": "^2.0.1" }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.1.tgz", - "integrity": "sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==" + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } }, - "node_modules/@simplewebauthn/browser": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.3.0.tgz", - "integrity": "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==", - "license": "MIT" + "node_modules/@testing-library/jest-dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } }, - "node_modules/@sinclair/typebox": { - "version": "0.24.51", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", - "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==" + "node_modules/@testing-library/jest-dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "node_modules/@sinonjs/commons": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", - "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", - "dependencies": { - "type-detect": "4.0.8" + "node_modules/@testing-library/jest-dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" } }, - "node_modules/@sinonjs/fake-timers": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", - "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", + "node_modules/@testing-library/jest-dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dependencies": { - "@sinonjs/commons": "^1.7.0" + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "license": "MIT" - }, - "node_modules/@squircle-js/react": { - "version": "1.2.17", - "resolved": "https://registry.npmjs.org/@squircle-js/react/-/react-1.2.17.tgz", - "integrity": "sha512-ejCDGx6P+dNt0GwIcv22N/A17++jd3RvRY38EAf8q3vpvFmv0oN8snYVzHI5X/G5+QqwSlF6eRXT57+7FKn+jA==", - "license": "MIT", + "node_modules/@testing-library/react": { + "version": "13.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.4.0.tgz", + "integrity": "sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==", "dependencies": { - "@radix-ui/react-slot": "^1.1.2", - "figma-squircle": "1.1.0" + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^8.5.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=12" }, "peerDependencies": { - "react": "^16.8 || ^17 || ^18 || ^19" + "react": "^18.0.0", + "react-dom": "^18.0.0" } }, - "node_modules/@surma/rollup-plugin-off-main-thread": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", - "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", + "node_modules/@testing-library/react/node_modules/@testing-library/dom": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", + "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", "dependencies": { - "ejs": "^3.1.6", - "json5": "^2.2.0", - "magic-string": "^0.25.0", - "string.prototype.matchall": "^4.0.6" + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=12" } }, - "node_modules/@svgr/babel-plugin-add-jsx-attribute": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz", - "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==", + "node_modules/@testing-library/react/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz", - "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" + "node_modules/@testing-library/react/node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dependencies": { + "deep-equal": "^2.0.5" } }, - "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz", - "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==", + "node_modules/@testing-library/react/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { "node": ">=10" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz", - "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" + "node_modules/@testing-library/react/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "node_modules/@svgr/babel-plugin-svg-dynamic-title": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz", - "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==", + "node_modules/@testing-library/react/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@testing-library/react/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "engines": { - "node": ">=10" + "node": ">=8" + } + }, + "node_modules/@testing-library/react/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" + "engines": { + "node": ">=8" } }, - "node_modules/@svgr/babel-plugin-svg-em-dimensions": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz", - "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==", + "node_modules/@testing-library/user-event": { + "version": "13.5.0", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", + "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, "engines": { - "node": ">=10" + "node": ">=10", + "npm": ">=6" }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tiptap/core": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.22.2.tgz", + "integrity": "sha512-atq35NkpeEphH6vNYJ0pTLLBA73FAbvTV9Ovd3AaTC5s99/KF5Q86zVJXvml8xPRcMGM6dLp+eSSd06oTscMSA==", + "license": "MIT", "funding": { "type": "github", - "url": "https://github.com/sponsors/gregberge" + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "^3.22.2" } }, - "node_modules/@svgr/babel-plugin-transform-react-native-svg": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz", - "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==", - "engines": { - "node": ">=10" - }, + "node_modules/@tiptap/extension-blockquote": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.22.2.tgz", + "integrity": "sha512-iTdlmGFcgxi4LKaOW2Rc9/yD83qTXgRm5BN3vCHWy5+TbEnReYxYqU5qKsbtTbKy30sO8TJTdAXTZ29uomShQQ==", + "license": "MIT", "funding": { "type": "github", - "url": "https://github.com/sponsors/gregberge" + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" } }, - "node_modules/@svgr/babel-plugin-transform-svg-component": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz", - "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==", - "engines": { - "node": ">=10" - }, + "node_modules/@tiptap/extension-bold": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.22.2.tgz", + "integrity": "sha512-bqsPJyKcT/RWse4e16U2EKhraR8a2+98TUuk1amG3yCyFJZStoO/j+pN0IqZdZZjr3WtxFyvwWp7Kc59UN+jUA==", + "license": "MIT", "funding": { "type": "github", - "url": "https://github.com/sponsors/gregberge" + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" } }, - "node_modules/@svgr/babel-preset": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz", - "integrity": "sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==", + "node_modules/@tiptap/extension-bubble-menu": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.22.2.tgz", + "integrity": "sha512-5hbyDOSkJwA2uh0v9Mm0Dd9bb9inx6tHBEDSH2tCB9Rm23poz3yOreB7SNX8xDMe5L0/PQesfWC14RitcmhKPg==", + "license": "MIT", + "optional": true, "dependencies": { - "@svgr/babel-plugin-add-jsx-attribute": "^5.4.0", - "@svgr/babel-plugin-remove-jsx-attribute": "^5.4.0", - "@svgr/babel-plugin-remove-jsx-empty-expression": "^5.0.1", - "@svgr/babel-plugin-replace-jsx-attribute-value": "^5.0.1", - "@svgr/babel-plugin-svg-dynamic-title": "^5.4.0", - "@svgr/babel-plugin-svg-em-dimensions": "^5.4.0", - "@svgr/babel-plugin-transform-react-native-svg": "^5.4.0", - "@svgr/babel-plugin-transform-svg-component": "^5.5.0" - }, - "engines": { - "node": ">=10" + "@floating-ui/dom": "^1.0.0" }, "funding": { "type": "github", - "url": "https://github.com/sponsors/gregberge" + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2", + "@tiptap/pm": "^3.22.2" } }, - "node_modules/@svgr/core": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz", - "integrity": "sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==", - "dependencies": { - "@svgr/plugin-jsx": "^5.5.0", - "camelcase": "^6.2.0", - "cosmiconfig": "^7.0.0" - }, - "engines": { - "node": ">=10" - }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.22.2.tgz", + "integrity": "sha512-llrTJnA72RGcWLLO+ro0QN4sjHynhaCerhpV+GZE/ATd8BqV/ekQFdBLJrvC/09My2XQfCwLsyCh92NPXUdELA==", + "license": "MIT", "funding": { "type": "github", - "url": "https://github.com/sponsors/gregberge" + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extension-list": "^3.22.2" } }, - "node_modules/@svgr/hast-util-to-babel-ast": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz", - "integrity": "sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==", - "dependencies": { - "@babel/types": "^7.12.6" - }, - "engines": { - "node": ">=10" - }, + "node_modules/@tiptap/extension-code": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.22.2.tgz", + "integrity": "sha512-iYFY+yzfYA9MKt7nupyW/PzqL9XC2D0mC8l1z2Y10i0/fGL8NbqIYjhNUAyXGqH3QWcI+DirI66842y2OadPOg==", + "license": "MIT", "funding": { "type": "github", - "url": "https://github.com/sponsors/gregberge" + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" } }, - "node_modules/@svgr/plugin-jsx": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz", - "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==", - "dependencies": { - "@babel/core": "^7.12.3", - "@svgr/babel-preset": "^5.5.0", - "@svgr/hast-util-to-babel-ast": "^5.5.0", - "svg-parser": "^2.0.2" - }, - "engines": { - "node": ">=10" - }, + "node_modules/@tiptap/extension-code-block": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.22.2.tgz", + "integrity": "sha512-PEwFlDyvtKF19WCrOFg77qJV9WqhvjCY4ZoXlHP9Hx0KTcOA8W39mtw8d4NWU5pLRK94yHKF1DVVL8UUkEOnww==", + "license": "MIT", "funding": { "type": "github", - "url": "https://github.com/sponsors/gregberge" + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2", + "@tiptap/pm": "^3.22.2" } }, - "node_modules/@svgr/plugin-svgo": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz", - "integrity": "sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==", - "dependencies": { - "cosmiconfig": "^7.0.0", - "deepmerge": "^4.2.2", - "svgo": "^1.2.2" + "node_modules/@tiptap/extension-document": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.22.2.tgz", + "integrity": "sha512-yPw9pQeVC4QDh86TuyKCZxxM4g0NAw7mEtGnAo6EpxaBQr1wyBr9yFpys+QTsQpRTmyTf1VHp4iTTLuWHMljIw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" }, - "engines": { - "node": ">=10" + "peerDependencies": { + "@tiptap/core": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.22.2.tgz", + "integrity": "sha512-sDv3fv4LtX0X4nqwh9Gn3C/aZXT+C2JlK7tJovPOpaYP/a6hr03Sn35X5moAfgMCSiWFygEvlTriqwmCsJuxog==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" }, + "peerDependencies": { + "@tiptap/extensions": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.22.2.tgz", + "integrity": "sha512-r0ZTeh9rNtj9Api+G0YyaB+tAKPDn7aYWg+qSrmAC5EyUPee6Zjn3zlw0q4renCeQflvNRK20xHM8zokC41jOA==", + "license": "MIT", + "optional": true, "funding": { "type": "github", - "url": "https://github.com/sponsors/gregberge" + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@floating-ui/dom": "^1.0.0", + "@tiptap/core": "^3.22.2", + "@tiptap/pm": "^3.22.2" } }, - "node_modules/@svgr/webpack": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz", - "integrity": "sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/plugin-transform-react-constant-elements": "^7.12.1", - "@babel/preset-env": "^7.12.1", - "@babel/preset-react": "^7.12.5", - "@svgr/core": "^5.5.0", - "@svgr/plugin-jsx": "^5.5.0", - "@svgr/plugin-svgo": "^5.5.0", - "loader-utils": "^2.0.0" + "node_modules/@tiptap/extension-gapcursor": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.22.2.tgz", + "integrity": "sha512-rR2OLrl/k2kj7xehaZHq0Y7T+1wy2DOTabir9LsTrktTFEcklrh9qY1KC6rEBkwMKaWrmignR1l39kS6RlKFNw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" }, - "engines": { - "node": ">=10" + "peerDependencies": { + "@tiptap/extensions": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.22.2.tgz", + "integrity": "sha512-ChsoqF4XRp6EWatTRlXL4LMFh/ggwRVCyt09brSfjJV5knFaXlECSa5/+rKLMLMULaj6dVlJqoAD15exgu2HHA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.22.2.tgz", + "integrity": "sha512-QPHLef+ikAyf7RVc4EdGeKxH4OEGb3ueCEwJ41RcYPtZ1BX9ueei7FC936guTdL1U7w3vQ65qfy86HznzkYgvw==", + "license": "MIT", "funding": { "type": "github", - "url": "https://github.com/sponsors/gregberge" + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" } }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.22.2.tgz", + "integrity": "sha512-Oz8KN5KJAWV1mFNE9UIWXdMD6xa5zPf/0yLsT8V4sgaRm+VsdFKllN58BY9qCZf/kIZbaOez5KkaoeAcm0MAZg==", "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" }, - "engines": { - "node": ">=18" + "peerDependencies": { + "@tiptap/core": "^3.22.2", + "@tiptap/pm": "^3.22.2" } }, - "node_modules/@testing-library/jest-dom": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", - "integrity": "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==", - "dependencies": { - "@adobe/css-tools": "^4.0.1", - "@babel/runtime": "^7.9.2", - "@types/testing-library__jest-dom": "^5.9.1", - "aria-query": "^5.0.0", - "chalk": "^3.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.5.6", - "lodash": "^4.17.15", - "redent": "^3.0.0" + "node_modules/@tiptap/extension-italic": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.22.2.tgz", + "integrity": "sha512-fmtQu2HDnV3sOZPdz0+1lOLI7UtrIhusohJj2UwOLQxG8qqhLwbvWx2OQTlfblgY0z+CjLRr6ANbNDxOTIblfg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" }, - "engines": { - "node": ">=8", - "npm": ">=6", - "yarn": ">=1" + "peerDependencies": { + "@tiptap/core": "^3.22.2" } }, - "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@tiptap/extension-link": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.22.2.tgz", + "integrity": "sha512-TXfSoKmng5pecvQUZqdsx6ICeob5V5hhYOj2vCEtjfcjWsyCndqFIl1w+Nt/yI5ehrFNOVPyj3ZvcELuuAW6pw==", + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "linkifyjs": "^4.3.2" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2", + "@tiptap/pm": "^3.22.2" } }, - "node_modules/@testing-library/jest-dom/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "node_modules/@tiptap/extension-list": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.22.2.tgz", + "integrity": "sha512-Vq9xScgkA2A3Zj9dQ4WUBKK7u7UCzeSFRz9FcKTQVZHRPbZoqFGnlRUVngqsE7JXrCOthXQ1dXxgk40nAsBFRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "@tiptap/core": "^3.22.2", + "@tiptap/pm": "^3.22.2" } }, - "node_modules/@testing-library/jest-dom/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" + "node_modules/@tiptap/extension-list-item": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.22.2.tgz", + "integrity": "sha512-Mk+iiLIFh8Pfuarr6mWfTO7QJbd2ZQd0nGNhNWXlGAO7DJCb4BP9nj4bEIJ17SbcykGRjsi4WMqY50z4MHXqKQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/@testing-library/jest-dom/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" + "peerDependencies": { + "@tiptap/extension-list": "^3.22.2" } }, - "node_modules/@testing-library/jest-dom/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" + "node_modules/@tiptap/extension-list-keymap": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.22.2.tgz", + "integrity": "sha512-TozU9V2vldMUPpTXnfLCO33EO06jLxn7uEJTMBnN4iX/dLV3cBVCbE4kHyDKS0sLd7joUeekS06vYP9uQb1hFw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" }, - "engines": { - "node": ">=8" + "peerDependencies": { + "@tiptap/extension-list": "^3.22.2" } }, - "node_modules/@testing-library/react": { - "version": "13.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.4.0.tgz", - "integrity": "sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==", - "dependencies": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^8.5.0", - "@types/react-dom": "^18.0.0" - }, - "engines": { - "node": ">=12" + "node_modules/@tiptap/extension-ordered-list": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.22.2.tgz", + "integrity": "sha512-K7qxoBKmsVkAd3kW64ZRCUPFrDcNGpXRDUBx9YgAO/bTfsfxtH2oil+igsUWGXPczpP4yoHPKjTfhpBpLjGl6Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "@tiptap/extension-list": "^3.22.2" } }, - "node_modules/@testing-library/react/node_modules/@testing-library/dom": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", - "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" + "node_modules/@tiptap/extension-paragraph": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.22.2.tgz", + "integrity": "sha512-EHZZzxVhvzEPDPWtRBF1YKhB+WCUjd1C2NhjHfL3Dl71PBqM3ZWA6qN7NDGPyNyGGWauui/NR/4X+5AfPqlHyA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" }, - "engines": { - "node": ">=12" + "peerDependencies": { + "@tiptap/core": "^3.22.2" } }, - "node_modules/@testing-library/react/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, + "node_modules/@tiptap/extension-placeholder": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.22.2.tgz", + "integrity": "sha512-xYw733CmSeG7MyYBDdV5NFiwlBdXXzw4Mvjb2t4QRXagkDbHeNY/LtKTcrtcMNfO4Jx0mwivGQZUIEC8oAfvxg==", + "license": "MIT", "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/extensions": "^3.22.2" } }, - "node_modules/@testing-library/react/node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dependencies": { - "deep-equal": "^2.0.5" + "node_modules/@tiptap/extension-strike": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.22.2.tgz", + "integrity": "sha512-YFC3elKU1L8PiGbcB6tqd/7vWPF5IbydJz0POJpHzSjstX+VfT8VsvS7ubxVuSIWQ11kGkH3mzX6LX8JHsHZxg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" } }, - "node_modules/@testing-library/react/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, + "node_modules/@tiptap/extension-text": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.22.2.tgz", + "integrity": "sha512-J1w7JwijfSD7ah0WfiwZ/DVWCIGT9x369RM4RJc57i44mIBElj7tl1dh+N5KPGOXKUup4gr7sSJAE38lgeaDMg==", + "license": "MIT", "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2" } }, - "node_modules/@testing-library/react/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" + "node_modules/@tiptap/extension-underline": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.22.2.tgz", + "integrity": "sha512-BaV6WOowxdkGTLWiU7DdZ3Twh633O4RGqwUM5dDas5LvaqL8AMWGTO8Wg9yAaaKXzd9MtKI1ZCqS/+MtzusgkQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" }, - "engines": { - "node": ">=7.0.0" + "peerDependencies": { + "@tiptap/core": "^3.22.2" } }, - "node_modules/@testing-library/react/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/@testing-library/react/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "engines": { - "node": ">=8" + "node_modules/@tiptap/extensions": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.22.2.tgz", + "integrity": "sha512-s7MZmm2Xdq+8feIXgY3v7gVpQ5ClqBZi20KheouS7KSbBlrY4fu2irYR1EGc6r1UUVaHMxEa+cx5knhx+mIPUw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2", + "@tiptap/pm": "^3.22.2" } }, - "node_modules/@testing-library/react/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/@tiptap/pm": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.22.2.tgz", + "integrity": "sha512-G2ENwIazoSKkAnN5MN5yN91TIZNFm6TxB74kPf3Empr2k9W51Hkcier70jHGpArhgcEaL4BVreuU1PRDRwCeGw==", + "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "prosemirror-changeset": "^2.3.0", + "prosemirror-collab": "^1.3.1", + "prosemirror-commands": "^1.6.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.4.1", + "prosemirror-inputrules": "^1.4.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.13.1", + "prosemirror-menu": "^1.2.4", + "prosemirror-model": "^1.24.1", + "prosemirror-schema-basic": "^1.2.3", + "prosemirror-schema-list": "^1.5.0", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.6.4", + "prosemirror-trailing-node": "^3.0.0", + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.38.1" }, - "engines": { - "node": ">=8" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" } }, - "node_modules/@testing-library/user-event": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-13.5.0.tgz", - "integrity": "sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg==", + "node_modules/@tiptap/react": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.22.2.tgz", + "integrity": "sha512-tyGKG69e/MkpoD/JTpVPz0XydEHxh1MSAYnLb3gRvyvBDv2r/veLea+cApkmjQaCfkKC/CWwTFXBYlOB0caSBA==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.12.5" + "@types/use-sync-external-store": "^0.0.6", + "fast-equals": "^5.3.3", + "use-sync-external-store": "^1.4.0" }, - "engines": { - "node": ">=10", - "npm": ">=6" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" + "optionalDependencies": { + "@tiptap/extension-bubble-menu": "^3.22.2", + "@tiptap/extension-floating-menu": "^3.22.2" + }, + "peerDependencies": { + "@tiptap/core": "^3.22.2", + "@tiptap/pm": "^3.22.2", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.22.2.tgz", + "integrity": "sha512-+CCKX8tOQ/ZPb2k/z6em4AQCFYAcdd8+0TOzPWiuLxRyCHRPBBVhnPsXOKgKwE4OO3E8BsezquuYRYRwsyzCqg==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^3.22.2", + "@tiptap/extension-blockquote": "^3.22.2", + "@tiptap/extension-bold": "^3.22.2", + "@tiptap/extension-bullet-list": "^3.22.2", + "@tiptap/extension-code": "^3.22.2", + "@tiptap/extension-code-block": "^3.22.2", + "@tiptap/extension-document": "^3.22.2", + "@tiptap/extension-dropcursor": "^3.22.2", + "@tiptap/extension-gapcursor": "^3.22.2", + "@tiptap/extension-hard-break": "^3.22.2", + "@tiptap/extension-heading": "^3.22.2", + "@tiptap/extension-horizontal-rule": "^3.22.2", + "@tiptap/extension-italic": "^3.22.2", + "@tiptap/extension-link": "^3.22.2", + "@tiptap/extension-list": "^3.22.2", + "@tiptap/extension-list-item": "^3.22.2", + "@tiptap/extension-list-keymap": "^3.22.2", + "@tiptap/extension-ordered-list": "^3.22.2", + "@tiptap/extension-paragraph": "^3.22.2", + "@tiptap/extension-strike": "^3.22.2", + "@tiptap/extension-text": "^3.22.2", + "@tiptap/extension-underline": "^3.22.2", + "@tiptap/extensions": "^3.22.2", + "@tiptap/pm": "^3.22.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" } }, "node_modules/@tootallnate/once": { @@ -5733,12 +5577,34 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, "node_modules/@types/lodash": { "version": "4.17.15", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz", "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==", "license": "MIT" }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -5814,16 +5680,6 @@ "@types/react": "*" } }, - "node_modules/@types/react-reconciler": { - "version": "0.28.9", - "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz", - "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-window": { "version": "1.8.8", "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", @@ -5915,12 +5771,11 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, - "node_modules/@types/webxr": { - "version": "0.5.24", - "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", - "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", - "license": "MIT", - "peer": true + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" }, "node_modules/@types/ws": { "version": "8.5.10", @@ -6706,19 +6561,6 @@ "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", "deprecated": "Use your platform's native atob() and btoa() methods instead" }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "license": "MIT", - "peer": true, - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -6876,13 +6718,6 @@ "ajv": "^6.9.1" } }, - "node_modules/anser": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", - "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", - "license": "MIT", - "peer": true - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -7632,16 +7467,6 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/babel-plugin-syntax-hermes-parser": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.32.0.tgz", - "integrity": "sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==", - "license": "MIT", - "peer": true, - "dependencies": { - "hermes-parser": "0.32.0" - } - }, "node_modules/babel-plugin-syntax-jsx": { "version": "6.18.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", @@ -8033,27 +7858,6 @@ "node": ">= 0.6.0" } }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "peer": true - }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -8244,31 +8048,6 @@ "node": ">= 0.4.0" } }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -8512,38 +8291,6 @@ "node": ">= 6" } }, - "node_modules/chrome-launcher": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", - "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@types/node": "*", - "escape-string-regexp": "^4.0.0", - "is-wsl": "^2.2.0", - "lighthouse-logger": "^1.0.0" - }, - "bin": { - "print-chrome-path": "bin/print-chrome-path.js" - }, - "engines": { - "node": ">=12.13.0" - } - }, - "node_modules/chrome-launcher/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -8552,47 +8299,6 @@ "node": ">=6.0" } }, - "node_modules/chromium-edge-launcher": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", - "integrity": "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@types/node": "*", - "escape-string-regexp": "^4.0.0", - "is-wsl": "^2.2.0", - "lighthouse-logger": "^1.0.0", - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - } - }, - "node_modules/chromium-edge-launcher/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/chromium-edge-launcher/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "peer": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -8815,22 +8521,6 @@ "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==" }, - "node_modules/connect": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", - "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "debug": "2.6.9", - "finalhandler": "1.1.2", - "parseurl": "~1.3.3", - "utils-merge": "1.0.1" - }, - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/connect-history-api-fallback": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", @@ -8839,65 +8529,6 @@ "node": ">=0.8" } }, - "node_modules/connect/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/connect/node_modules/finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "license": "MIT", - "peer": true, - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/connect/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT", - "peer": true - }, - "node_modules/connect/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "license": "MIT", - "peer": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/connect/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -8988,6 +8619,12 @@ "node": ">=10" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -11079,16 +10716,6 @@ "node": ">= 0.6" } }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -11146,13 +10773,6 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "license": "Apache-2.0", - "peer": true - }, "node_modules/express": { "version": "4.18.2", "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", @@ -11217,6 +10837,15 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -11285,19 +10914,6 @@ "node": ">=0.8.0" } }, - "node_modules/fb-dotslash": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/fb-dotslash/-/fb-dotslash-0.5.8.tgz", - "integrity": "sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA==", - "license": "(MIT OR Apache-2.0)", - "peer": true, - "bin": { - "dotslash": "bin/dotslash" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -11474,13 +11090,6 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" }, - "node_modules/flow-enums-runtime": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", - "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==", - "license": "MIT", - "peer": true - }, "node_modules/follow-redirects": { "version": "1.15.5", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", @@ -12188,30 +11797,6 @@ "he": "bin/he" } }, - "node_modules/hermes-compiler": { - "version": "250829098.0.9", - "resolved": "https://registry.npmjs.org/hermes-compiler/-/hermes-compiler-250829098.0.9.tgz", - "integrity": "sha512-hZ5O7PDz1vQ99TS7HD3FJ9zVynfU1y+VWId6U1Pldvd8hmAYrNec/XLPYJKD3dLOW6NXak6aAQAuMuSo3ji0tQ==", - "license": "MIT", - "peer": true - }, - "node_modules/hermes-estree": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz", - "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==", - "license": "MIT", - "peer": true - }, - "node_modules/hermes-parser": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz", - "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", - "license": "MIT", - "peer": true, - "dependencies": { - "hermes-estree": "0.32.0" - } - }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", @@ -12547,27 +12132,6 @@ "node": ">=4" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause", - "peer": true - }, "node_modules/ignore": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", @@ -12576,22 +12140,6 @@ "node": ">= 4" } }, - "node_modules/image-size": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", - "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", - "license": "MIT", - "peer": true, - "dependencies": { - "queue": "6.0.2" - }, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, "node_modules/immer": { "version": "9.0.21", "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", @@ -13336,19 +12884,6 @@ "set-function-name": "^2.0.1" } }, - "node_modules/its-fine": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz", - "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/react-reconciler": "^0.28.0" - }, - "peerDependencies": { - "react": ">=18.0" - } - }, "node_modules/jackspeak": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", @@ -15349,13 +14884,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsc-safe-url": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz", - "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==", - "license": "0BSD", - "peer": true - }, "node_modules/jsdom": { "version": "16.7.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", @@ -15561,27 +15089,6 @@ "node": ">= 8" } }, - "node_modules/konva": { - "version": "10.2.3", - "resolved": "https://registry.npmjs.org/konva/-/konva-10.2.3.tgz", - "integrity": "sha512-NDGeIxm2nsQcp6oqZKS9T764JEi53RpQvpUxV2EK7Awm49fwdd1+EB1Nq1nyspRc0hOAKyKssoTFvPaKwiSUog==", - "funding": [ - { - "type": "patreon", - "url": "https://www.patreon.com/lavrton" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/konva" - }, - { - "type": "github", - "url": "https://github.com/sponsors/lavrton" - } - ], - "license": "MIT", - "peer": true - }, "node_modules/language-subtag-registry": { "version": "0.3.22", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz", @@ -15627,34 +15134,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lighthouse-logger": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", - "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "debug": "^2.6.9", - "marky": "^1.2.2" - } - }, - "node_modules/lighthouse-logger/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/lighthouse-logger/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT", - "peer": true - }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -15668,6 +15147,21 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/linkifyjs": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", + "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", + "license": "MIT" + }, "node_modules/loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", @@ -15728,13 +15222,6 @@ "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" }, - "node_modules/lodash.throttle": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", - "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", - "license": "MIT", - "peer": true - }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -15836,6 +15323,41 @@ "tmpl": "1.0.5" } }, + "node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/marked": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.2.tgz", @@ -15848,13 +15370,6 @@ "node": ">= 20" } }, - "node_modules/marky": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", - "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", - "license": "Apache-2.0", - "peer": true - }, "node_modules/math-expression-evaluator": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.4.0.tgz", @@ -15866,6 +15381,12 @@ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==" }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -15917,2156 +15438,1900 @@ "node": ">= 0.6" } }, - "node_modules/metro": { - "version": "0.83.5", - "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.5.tgz", - "integrity": "sha512-BgsXevY1MBac/3ZYv/RfNFf/4iuW9X7f4H8ZNkiH+r667HD9sVujxcmu4jvEzGCAm4/WyKdZCuyhAcyhTHOucQ==", + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "license": "MIT", - "peer": true, "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/core": "^7.25.2", - "@babel/generator": "^7.29.1", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "accepts": "^2.0.0", - "chalk": "^4.0.0", - "ci-info": "^2.0.0", - "connect": "^3.6.5", - "debug": "^4.4.0", - "error-stack-parser": "^2.0.6", - "flow-enums-runtime": "^0.0.6", - "graceful-fs": "^4.2.4", - "hermes-parser": "0.33.3", - "image-size": "^1.0.2", - "invariant": "^2.2.4", - "jest-worker": "^29.7.0", - "jsc-safe-url": "^0.2.2", - "lodash.throttle": "^4.1.1", - "metro-babel-transformer": "0.83.5", - "metro-cache": "0.83.5", - "metro-cache-key": "0.83.5", - "metro-config": "0.83.5", - "metro-core": "0.83.5", - "metro-file-map": "0.83.5", - "metro-resolver": "0.83.5", - "metro-runtime": "0.83.5", - "metro-source-map": "0.83.5", - "metro-symbolicate": "0.83.5", - "metro-transform-plugins": "0.83.5", - "metro-transform-worker": "0.83.5", - "mime-types": "^3.0.1", - "nullthrows": "^1.1.1", - "serialize-error": "^2.1.0", - "source-map": "^0.5.6", - "throat": "^5.0.0", - "ws": "^7.5.10", - "yargs": "^17.6.2" - }, - "bin": { - "metro": "src/cli.js" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": ">=20.19.4" + "node": ">=8.6" } }, - "node_modules/metro-babel-transformer": { - "version": "0.83.5", - "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.83.5.tgz", - "integrity": "sha512-d9FfmgUEVejTiSb7bkQeLRGl6aeno2UpuPm3bo3rCYwxewj03ymvOn8s8vnS4fBqAPQ+cE9iQM40wh7nGXR+eA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/core": "^7.25.2", - "flow-enums-runtime": "^0.0.6", - "hermes-parser": "0.33.3", - "nullthrows": "^1.1.1" + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" }, "engines": { - "node": ">=20.19.4" + "node": ">=4" } }, - "node_modules/metro-babel-transformer/node_modules/hermes-estree": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz", - "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==", - "license": "MIT", - "peer": true + "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==", + "engines": { + "node": ">= 0.6" + } }, - "node_modules/metro-babel-transformer/node_modules/hermes-parser": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz", - "integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==", - "license": "MIT", - "peer": true, - "dependencies": { - "hermes-estree": "0.33.3" - } - }, - "node_modules/metro-cache": { - "version": "0.83.5", - "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.5.tgz", - "integrity": "sha512-oH+s4U+IfZyg8J42bne2Skc90rcuESIYf86dYittcdWQtPfcaFXWpByPyTuWk3rR1Zz3Eh5HOrcVImfEhhJLng==", - "license": "MIT", - "peer": true, + "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==", "dependencies": { - "exponential-backoff": "^3.1.1", - "flow-enums-runtime": "^0.0.6", - "https-proxy-agent": "^7.0.5", - "metro-core": "0.83.5" + "mime-db": "1.52.0" }, "engines": { - "node": ">=20.19.4" + "node": ">= 0.6" } }, - "node_modules/metro-cache-key": { - "version": "0.83.5", - "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.83.5.tgz", - "integrity": "sha512-Ycl8PBajB7bhbAI7Rt0xEyiF8oJ0RWX8EKkolV1KfCUlC++V/GStMSGpPLwnnBZXZWkCC5edBPzv1Hz1Yi0Euw==", - "license": "MIT", - "peer": true, - "dependencies": { - "flow-enums-runtime": "^0.0.6" - }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "engines": { - "node": ">=20.19.4" + "node": ">=6" } }, - "node_modules/metro-cache/node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "peer": true, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", "engines": { - "node": ">= 14" + "node": ">=4" } }, - "node_modules/metro-cache/node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "peer": true, + "node_modules/mini-css-extract-plugin": { + "version": "2.7.6", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz", + "integrity": "sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==", "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" + "schema-utils": "^4.0.0" }, "engines": { - "node": ">= 14" - } - }, - "node_modules/metro-config": { - "version": "0.83.5", - "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.83.5.tgz", - "integrity": "sha512-JQ/PAASXH7yczgV6OCUSRhZYME+NU8NYjI2RcaG5ga4QfQ3T/XdiLzpSb3awWZYlDCcQb36l4Vl7i0Zw7/Tf9w==", - "license": "MIT", - "peer": true, - "dependencies": { - "connect": "^3.6.5", - "flow-enums-runtime": "^0.0.6", - "jest-validate": "^29.7.0", - "metro": "0.83.5", - "metro-cache": "0.83.5", - "metro-core": "0.83.5", - "metro-runtime": "0.83.5", - "yaml": "^2.6.1" + "node": ">= 12.13.0" }, - "engines": { - "node": ">=20.19.4" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" } }, - "node_modules/metro-config/node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "license": "MIT", - "peer": true, + "node_modules/mini-css-extract-plugin/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dependencies": { - "@sinclair/typebox": "^0.27.8" + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/metro-config/node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "license": "MIT", - "peer": true, + "node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "fast-deep-equal": "^3.1.3" }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "peerDependencies": { + "ajv": "^8.8.2" } }, - "node_modules/metro-config/node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "license": "MIT", - "peer": true - }, - "node_modules/metro-config/node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/yargs-parser": "*" - } + "node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, - "node_modules/metro-config/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "peer": true, + "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dependencies": { - "color-convert": "^2.0.1" + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">=8" + "node": ">= 12.13.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/metro-config/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" }, - "node_modules/metro-config/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "peer": true, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dependencies": { - "color-name": "~1.1.4" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=7.0.0" + "node": "*" } }, - "node_modules/metro-config/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true - }, - "node_modules/metro-config/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/metro-config/node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", - "license": "MIT", - "peer": true, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=16 || 14 >=14.17" } }, - "node_modules/metro-config/node_modules/jest-validate": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", - "license": "MIT", - "peer": true, + "node_modules/mitt": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz", + "integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==", + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "leven": "^3.1.0", - "pretty-format": "^29.7.0" + "minimist": "^1.2.6" }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "bin": { + "mkdirp": "bin/cmd.js" } }, - "node_modules/metro-config/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", "license": "MIT", - "peer": true, "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "motion-utils": "^11.18.1" } }, - "node_modules/metro-config/node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } + "node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", + "license": "MIT" }, - "node_modules/metro-config/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT", - "peer": true + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/metro-config/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "peer": true, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", "dependencies": { - "has-flag": "^4.0.0" + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/metro-config/node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "license": "ISC", - "peer": true, "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" + "multicast-dns": "cli.js" } }, - "node_modules/metro-core": { - "version": "0.83.5", - "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.83.5.tgz", - "integrity": "sha512-YcVcLCrf0ed4mdLa82Qob0VxYqfhmlRxUS8+TO4gosZo/gLwSvtdeOjc/Vt0pe/lvMNrBap9LlmvZM8FIsMgJQ==", - "license": "MIT", - "peer": true, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "dependencies": { - "flow-enums-runtime": "^0.0.6", - "lodash.throttle": "^4.1.1", - "metro-resolver": "0.83.5" - }, - "engines": { - "node": ">=20.19.4" + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" } }, - "node_modules/metro-file-map": { - "version": "0.83.5", - "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.83.5.tgz", - "integrity": "sha512-ZEt8s3a1cnYbn40nyCD+CsZdYSlwtFh2kFym4lo+uvfM+UMMH+r/BsrC6rbNClSrt+B7rU9T+Te/sh/NL8ZZKQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "debug": "^4.4.0", - "fb-watchman": "^2.0.0", - "flow-enums-runtime": "^0.0.6", - "graceful-fs": "^4.2.4", - "invariant": "^2.2.4", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "nullthrows": "^1.1.1", - "walker": "^1.0.7" + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": ">=20.19.4" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/metro-file-map/node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.6" } }, - "node_modules/metro-file-map/node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "license": "MIT", - "peer": true, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 6.13.0" } }, - "node_modules/metro-file-map/node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "license": "MIT", - "peer": true + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" }, - "node_modules/metro-file-map/node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/yargs-parser": "*" + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/metro-file-map/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "peer": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=0.10.0" } }, - "node_modules/metro-file-map/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/metro-file-map/node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "peer": true, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dependencies": { - "has-flag": "^4.0.0" + "path-key": "^3.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/metro-file-map/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "peer": true, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dependencies": { - "color-name": "~1.1.4" + "boolbase": "^1.0.0" }, - "engines": { - "node": ">=7.0.0" + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "node_modules/metro-file-map/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "node_modules/nwsapi": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==" }, - "node_modules/metro-file-map/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "^2.1.3" - }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=0.10.0" } }, - "node_modules/metro-file-map/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "peer": true, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "engines": { - "node": ">=8" + "node": ">= 6" } }, - "node_modules/metro-file-map/node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "license": "MIT", - "peer": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/metro-file-map/node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "license": "MIT", - "peer": true, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/metro-file-map/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT", - "peer": true + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } }, - "node_modules/metro-file-map/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "peer": true, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dependencies": { - "has-flag": "^4.0.0" + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/metro-minify-terser": { - "version": "0.83.5", - "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.83.5.tgz", - "integrity": "sha512-Toe4Md1wS1PBqbvB0cFxBzKEVyyuYTUb0sgifAZh/mSvLH84qA1NAWik9sISWatzvfWf3rOGoUoO5E3f193a3Q==", - "license": "MIT", - "peer": true, + "node_modules/object.entries": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", + "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", "dependencies": { - "flow-enums-runtime": "^0.0.6", - "terser": "^5.15.0" + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { - "node": ">=20.19.4" + "node": ">= 0.4" } }, - "node_modules/metro-resolver": { - "version": "0.83.5", - "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.83.5.tgz", - "integrity": "sha512-7p3GtzVUpbAweJeCcUJihJeOQl1bDuimO5ueo1K0BUpUtR41q5EilbQ3klt16UTPPMpA+tISWBtsrqU556mY1A==", - "license": "MIT", - "peer": true, + "node_modules/object.fromentries": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", + "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", "dependencies": { - "flow-enums-runtime": "^0.0.6" - }, - "engines": { - "node": ">=20.19.4" + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/metro-runtime": { - "version": "0.83.5", - "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.83.5.tgz", - "integrity": "sha512-f+b3ue9AWTVlZe2Xrki6TAoFtKIqw30jwfk7GQ1rDUBQaE0ZQ+NkiMEtb9uwH7uAjJ87U7Tdx1Jg1OJqUfEVlA==", - "license": "MIT", - "peer": true, + "node_modules/object.getownpropertydescriptors": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.7.tgz", + "integrity": "sha512-PrJz0C2xJ58FNn11XV2lr4Jt5Gzl94qpy9Lu0JlfEj14z88sqbSBJCBEzdlNUCzY2gburhbrwOZ5BHCmuNUy0g==", "dependencies": { - "@babel/runtime": "^7.25.0", - "flow-enums-runtime": "^0.0.6" + "array.prototype.reduce": "^1.0.6", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "safe-array-concat": "^1.0.0" }, "engines": { - "node": ">=20.19.4" + "node": ">= 0.8" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/metro-source-map": { - "version": "0.83.5", - "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.83.5.tgz", - "integrity": "sha512-VT9bb2KO2/4tWY9Z2yeZqTUao7CicKAOps9LUg2aQzsz+04QyuXL3qgf1cLUVRjA/D6G5u1RJAlN1w9VNHtODQ==", - "license": "MIT", - "peer": true, + "node_modules/object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", "dependencies": { - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "flow-enums-runtime": "^0.0.6", - "invariant": "^2.2.4", - "metro-symbolicate": "0.83.5", - "nullthrows": "^1.1.1", - "ob1": "0.83.5", - "source-map": "^0.5.6", - "vlq": "^1.0.0" + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, + "node_modules/object.hasown": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", + "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==", + "dependencies": { + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, - "engines": { - "node": ">=20.19.4" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/metro-source-map/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "license": "BSD-3-Clause", - "peer": true, + "node_modules/object.values": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", + "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/metro-symbolicate": { - "version": "0.83.5", - "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.83.5.tgz", - "integrity": "sha512-EMIkrjNRz/hF+p0RDdxoE60+dkaTLPN3vaaGkFmX5lvFdO6HPfHA/Ywznzkev+za0VhPQ5KSdz49/MALBRteHA==", - "license": "MIT", - "peer": true, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dependencies": { - "flow-enums-runtime": "^0.0.6", - "invariant": "^2.2.4", - "metro-source-map": "0.83.5", - "nullthrows": "^1.1.1", - "source-map": "^0.5.6", - "vlq": "^1.0.0" - }, - "bin": { - "metro-symbolicate": "src/index.js" + "ee-first": "1.1.1" }, "engines": { - "node": ">=20.19.4" + "node": ">= 0.8" } }, - "node_modules/metro-symbolicate/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "license": "BSD-3-Clause", - "peer": true, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", "engines": { - "node": ">=0.10.0" + "node": ">= 0.8" } }, - "node_modules/metro-transform-plugins": { - "version": "0.83.5", - "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.83.5.tgz", - "integrity": "sha512-KxYKzZL+lt3Os5H2nx7YkbkWVduLZL5kPrE/Yq+Prm/DE1VLhpfnO6HtPs8vimYFKOa58ncl60GpoX0h7Wm0Vw==", - "license": "MIT", - "peer": true, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dependencies": { - "@babel/core": "^7.25.2", - "@babel/generator": "^7.29.1", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "flow-enums-runtime": "^0.0.6", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": ">=20.19.4" + "wrappy": "1" } }, - "node_modules/metro-transform-worker": { - "version": "0.83.5", - "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.83.5.tgz", - "integrity": "sha512-8N4pjkNXc6ytlP9oAM6MwqkvUepNSW39LKYl9NjUMpRDazBQ7oBpQDc8Sz4aI8jnH6AGhF7s1m/ayxkN1t04yA==", - "license": "MIT", - "peer": true, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dependencies": { - "@babel/core": "^7.25.2", - "@babel/generator": "^7.29.1", - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "flow-enums-runtime": "^0.0.6", - "metro": "0.83.5", - "metro-babel-transformer": "0.83.5", - "metro-cache": "0.83.5", - "metro-cache-key": "0.83.5", - "metro-minify-terser": "0.83.5", - "metro-source-map": "0.83.5", - "metro-transform-plugins": "0.83.5", - "nullthrows": "^1.1.1" + "mimic-fn": "^2.1.0" }, "engines": { - "node": ">=20.19.4" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/metro/node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "license": "MIT", - "peer": true, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "dependencies": { - "@sinclair/typebox": "^0.27.8" + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/metro/node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "license": "MIT", - "peer": true, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.8.0" } }, - "node_modules/metro/node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "license": "MIT", - "peer": true + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" }, - "node_modules/metro/node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/yargs-parser": "*" + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/metro/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "peer": true, - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, - "node_modules/metro/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "peer": true, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dependencies": { - "color-convert": "^2.0.1" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/metro/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "peer": true, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "p-limit": "^3.0.2" }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/metro/node_modules/ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "license": "MIT", - "peer": true - }, - "node_modules/metro/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "peer": true, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "@types/retry": "0.12.0", + "retry": "^0.13.1" }, "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/metro/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "color-name": "~1.1.4" - }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "engines": { - "node": ">=7.0.0" + "node": ">=6" } }, - "node_modules/metro/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } }, - "node_modules/metro/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "peer": true, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dependencies": { - "ms": "^2.1.3" + "callsites": "^3.0.0" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=6" } }, - "node_modules/metro/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", "license": "MIT", - "peer": true, - "engines": { + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/metro/node_modules/hermes-estree": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.33.3.tgz", - "integrity": "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==", - "license": "MIT", - "peer": true + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" }, - "node_modules/metro/node_modules/hermes-parser": { - "version": "0.33.3", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.33.3.tgz", - "integrity": "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==", - "license": "MIT", - "peer": true, - "dependencies": { - "hermes-estree": "0.33.3" + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" } }, - "node_modules/metro/node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "license": "MIT", - "peer": true, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "no-case": "^3.0.4", + "tslib": "^2.0.3" } }, - "node_modules/metro/node_modules/jest-util/node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "peer": true, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "engines": { "node": ">=8" } }, - "node_modules/metro/node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=0.10.0" } }, - "node_modules/metro/node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "peer": true, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", "dependencies": { - "has-flag": "^4.0.0" + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/metro/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "peer": true, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", + "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", "engines": { - "node": ">= 0.6" + "node": "14 || >=16.14" } }, - "node_modules/metro/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "peer": true, - "dependencies": { - "mime-db": "^1.54.0" - }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "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==" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "engines": { - "node": ">=18" + "node": ">=8.6" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/metro/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT", - "peer": true + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } }, - "node_modules/metro/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "peer": true, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "engines": { - "node": ">= 0.6" + "node": ">= 6" } }, - "node_modules/metro/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "license": "BSD-3-Clause", - "peer": true, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dependencies": { + "find-up": "^4.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/metro/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "peer": true, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dependencies": { - "has-flag": "^4.0.0" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/metro/node_modules/throat": { + "node_modules/pkg-dir/node_modules/locate-path": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", - "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", - "license": "MIT", - "peer": true - }, - "node_modules/metro/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "peer": true, + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "p-locate": "^4.1.0" }, "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/metro/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "peer": true, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, "engines": { - "node": ">=12" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "p-limit": "^2.2.0" }, "engines": { - "node": ">=8.6" + "node": ">=8" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dependencies": { + "find-up": "^3.0.0" }, "engines": { - "node": ">=4" + "node": ">=8" } }, - "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==", + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dependencies": { + "locate-path": "^3.0.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=6" } }, - "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==", + "node_modules/pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "dependencies": { - "mime-db": "1.52.0" + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">=6" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "node_modules/pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, "engines": { "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "node_modules/pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", "engines": { "node": ">=4" } }, - "node_modules/mini-css-extract-plugin": { - "version": "2.7.6", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz", - "integrity": "sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==", + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/postcss": { + "version": "8.4.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", + "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { - "schema-utils": "^4.0.0" + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" }, "engines": { - "node": ">= 12.13.0" + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz", + "integrity": "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/webpack" + "url": "https://opencollective.com/csstools" }, "peerDependencies": { - "webpack": "^5.0.0" + "postcss": "^8.2" } }, - "node_modules/mini-css-extract-plugin/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "node_modules/postcss-browser-comments": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz", + "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==", + "engines": { + "node": ">=8" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "peerDependencies": { + "browserslist": ">=4", + "postcss": ">=8" } }, - "node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "node_modules/postcss-calc": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", + "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", "dependencies": { - "fast-deep-equal": "^3.1.3" + "postcss-selector-parser": "^6.0.9", + "postcss-value-parser": "^4.2.0" }, "peerDependencies": { - "ajv": "^8.8.2" + "postcss": "^8.2.2" } }, - "node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + "node_modules/postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=7.6.0" + }, + "peerDependencies": { + "postcss": "^8.4.6" + } }, - "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "node_modules/postcss-color-functional-notation": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz", + "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==", "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">= 12.13.0" + "node": "^12 || ^14 || >=16" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/webpack" + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" } }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/postcss-color-hex-alpha": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz", + "integrity": "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==", "dependencies": { - "brace-expansion": "^1.1.7" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "node": "^12 || ^14 || >=16" + }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" } }, - "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "node_modules/postcss-color-rebeccapurple": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz", + "integrity": "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" } }, - "node_modules/mitt": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz", - "integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==", - "license": "MIT" - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "node_modules/postcss-colormin": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz", + "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==", "dependencies": { - "minimist": "^1.2.6" + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" }, - "bin": { - "mkdirp": "bin/cmd.js" + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/motion-dom": { - "version": "11.18.1", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", - "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", - "license": "MIT", + "node_modules/postcss-convert-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", + "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", "dependencies": { - "motion-utils": "^11.18.1" + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/motion-utils": { - "version": "11.18.1", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", - "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "node_modules/postcss-custom-media": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz", + "integrity": "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==", "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" + "postcss-value-parser": "^4.2.0" }, - "bin": { - "multicast-dns": "cli.js" + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.3" } }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "node_modules/postcss-custom-properties": { + "version": "12.1.11", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz", + "integrity": "sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==", "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, - "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" - }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "node_modules/postcss-custom-selectors": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz", + "integrity": "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==", + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, "engines": { - "node": ">= 0.6" + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.3" } }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" - }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "node_modules/postcss-dir-pseudo-class": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz", + "integrity": "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==", "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" } }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "node_modules/postcss-discard-comments": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", "engines": { - "node": ">= 6.13.0" + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "node_modules/postcss-discard-duplicates": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", "engines": { - "node": ">=0.10.0" + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "node_modules/postcss-discard-empty": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", "engines": { - "node": ">=0.10.0" + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "node_modules/postcss-discard-overridden": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", "engines": { - "node": ">=10" + "node": "^10 || ^12 || >=14.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "node_modules/postcss-double-position-gradients": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz", + "integrity": "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==", "dependencies": { - "path-key": "^3.0.0" + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dependencies": { - "boolbase": "^1.0.0" + "node": "^12 || ^14 || >=16" }, "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" } }, - "node_modules/nullthrows": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", - "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", - "license": "MIT", - "peer": true - }, - "node_modules/nwsapi": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", - "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==" - }, - "node_modules/ob1": { - "version": "0.83.5", - "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.5.tgz", - "integrity": "sha512-vNKPYC8L5ycVANANpF/S+WZHpfnRWKx/F3AYP4QMn6ZJTh+l2HOrId0clNkEmua58NB9vmI9Qh7YOoV/4folYg==", - "license": "MIT", - "peer": true, + "node_modules/postcss-env-function": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz", + "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", "dependencies": { - "flow-enums-runtime": "^0.0.6" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=20.19.4" + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" } }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" + "node_modules/postcss-flexbugs-fixes": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", + "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==", + "peerDependencies": { + "postcss": "^8.1.4" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "node_modules/postcss-focus-visible": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", + "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, "engines": { - "node": ">= 6" - } - }, - "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" } }, - "node_modules/object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "node_modules/postcss-focus-within": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", + "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "postcss-selector-parser": "^6.0.9" }, "engines": { - "node": ">= 0.4" + "node": "^12 || ^14 || >=16" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "postcss": "^8.4" } }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "engines": { - "node": ">= 0.4" + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, + "node_modules/postcss-gap-properties": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", + "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", "engines": { - "node": ">= 0.4" + "node": "^12 || ^14 || >=16" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz", - "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "type": "opencollective", + "url": "https://opencollective.com/csstools" }, - "engines": { - "node": ">= 0.4" + "peerDependencies": { + "postcss": "^8.2" } }, - "node_modules/object.fromentries": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz", - "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==", + "node_modules/postcss-image-set-function": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz", + "integrity": "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">= 0.4" + "node": "^12 || ^14 || >=16" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" } }, - "node_modules/object.getownpropertydescriptors": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.7.tgz", - "integrity": "sha512-PrJz0C2xJ58FNn11XV2lr4Jt5Gzl94qpy9Lu0JlfEj14z88sqbSBJCBEzdlNUCzY2gburhbrwOZ5BHCmuNUy0g==", + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "dependencies": { - "array.prototype.reduce": "^1.0.6", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "safe-array-concat": "^1.0.0" + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" }, "engines": { - "node": ">= 0.8" + "node": ">=14.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "postcss": "^8.0.0" } }, - "node_modules/object.groupby": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", - "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1" + "node_modules/postcss-initial": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", + "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", + "peerDependencies": { + "postcss": "^8.0.0" } }, - "node_modules/object.hasown": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", - "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==", + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "dependencies": { - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" } }, - "node_modules/object.values": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz", - "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==", + "node_modules/postcss-lab-function": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz", + "integrity": "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">= 0.4" + "node": "^12 || ^14 || >=16" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" } }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { - "ee-first": "1.1.1" + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" }, "engines": { - "node": ">= 0.8" + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } } }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", "engines": { - "node": ">= 0.8" + "node": ">=14" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "engines": { + "node": ">= 14" } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "node_modules/postcss-loader": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", + "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", "dependencies": { - "mimic-fn": "^2.1.0" + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.5" }, "engines": { - "node": ">=6" + "node": ">= 12.13.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" } }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" + "node_modules/postcss-logical": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", + "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", + "engines": { + "node": "^12 || ^14 || >=16" }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-media-minmax": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", + "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", "engines": { - "node": ">=12" + "node": ">=10.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "node_modules/postcss-merge-longhand": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", + "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" + "postcss-value-parser": "^4.2.0", + "stylehacks": "^5.1.1" }, "engines": { - "node": ">= 0.8.0" + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==", + "node_modules/postcss-merge-rules": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz", + "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^3.1.0", + "postcss-selector-parser": "^6.0.5" + }, "engines": { - "node": ">=0.10.0" + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "node_modules/postcss-minify-font-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", + "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, "engines": { - "node": ">=0.10.0" + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "node_modules/postcss-minify-gradients": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", + "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", "dependencies": { - "yocto-queue": "^0.1.0" + "colord": "^2.9.1", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=10" + "node": "^10 || ^12 || >=14.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "node_modules/postcss-minify-params": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", + "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", "dependencies": { - "p-limit": "^3.0.2" + "browserslist": "^4.21.4", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=10" + "node": "^10 || ^12 || >=14.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "node_modules/postcss-minify-selectors": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" + "postcss-selector-parser": "^6.0.5" }, "engines": { - "node": ">=8" + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", "engines": { - "node": ">=6" + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/param-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", + "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "node_modules/postcss-modules-scope": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.0.tgz", + "integrity": "sha512-SaIbK8XW+MZbd0xHPf7kdfA/3eOt7vxJ72IRecn3EzuZVLr1r0orzf0MX/pN8m+NMDoo6X/SQd8oeKqGZd8PXg==", "dependencies": { - "callsites": "^3.0.0" + "postcss-selector-parser": "^6.0.4" }, "engines": { - "node": ">=6" + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/parse-entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", - "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", - "license": "MIT", + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", "dependencies": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" + "icss-utils": "^5.0.0" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" + "postcss-selector-parser": "^6.0.11" }, "engines": { - "node": ">=8" + "node": ">=12.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" } }, - "node_modules/parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/pascal-case": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "node_modules/postcss-nesting": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz", + "integrity": "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==", "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^12 || ^14 || >=16" }, "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", - "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "engines": { - "node": ">=8" - } - }, - "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==" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" + "type": "opencollective", + "url": "https://opencollective.com/csstools" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "peerDependencies": { + "postcss": "^8.2" } }, - "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "node_modules/postcss-normalize": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-10.0.1.tgz", + "integrity": "sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA==", + "dependencies": { + "@csstools/normalize.css": "*", + "postcss-browser-comments": "^4", + "sanitize.css": "*" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 12" + }, + "peerDependencies": { + "browserslist": ">= 4", + "postcss": ">= 8" } }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "node_modules/postcss-normalize-charset": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", "engines": { - "node": ">= 6" + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "node_modules/postcss-normalize-display-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", + "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", "dependencies": { - "find-up": "^4.0.0" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=8" + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/postcss-normalize-positions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", + "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=8" + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/postcss-normalize-repeat-style": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", + "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", "dependencies": { - "p-locate": "^4.1.0" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=8" + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/postcss-normalize-string": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", + "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", "dependencies": { - "p-try": "^2.0.0" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=6" + "node": "^10 || ^12 || >=14.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/postcss-normalize-timing-functions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", + "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", "dependencies": { - "p-limit": "^2.2.0" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=8" + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/pkg-up": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", - "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "node_modules/postcss-normalize-unicode": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", + "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", "dependencies": { - "find-up": "^3.0.0" + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=8" + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/pkg-up/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "node_modules/postcss-normalize-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", + "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", "dependencies": { - "locate-path": "^3.0.0" + "normalize-url": "^6.0.1", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=6" + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/pkg-up/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "node_modules/postcss-normalize-whitespace": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", + "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-up/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dependencies": { - "p-limit": "^2.0.0" + "node": "^10 || ^12 || >=14.0" }, - "engines": { - "node": ">=6" - } - }, - "node_modules/pkg-up/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "engines": { - "node": ">=4" - } - }, - "node_modules/popper.js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", - "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", - "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/postcss": { - "version": "8.4.32", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", - "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", + "node_modules/postcss-opacity-percentage": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz", + "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==", "funding": [ { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" + "type": "kofi", + "url": "https://ko-fi.com/mrcgrtz" }, { - "type": "github", - "url": "https://github.com/sponsors/ai" + "type": "liberapay", + "url": "https://liberapay.com/mrcgrtz" } ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-attribute-case-insensitive": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz", - "integrity": "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, "engines": { "node": "^12 || ^14 || >=16" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, "peerDependencies": { "postcss": "^8.2" } }, - "node_modules/postcss-browser-comments": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz", - "integrity": "sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg==", - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "browserslist": ">=4", - "postcss": ">=8" - } - }, - "node_modules/postcss-calc": { - "version": "8.2.4", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", - "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", + "node_modules/postcss-ordered-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", + "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", "dependencies": { - "postcss-selector-parser": "^6.0.9", + "cssnano-utils": "^3.1.0", "postcss-value-parser": "^4.2.0" }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, "peerDependencies": { - "postcss": "^8.2.2" + "postcss": "^8.2.15" } }, - "node_modules/postcss-clamp": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", - "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "node_modules/postcss-overflow-shorthand": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz", + "integrity": "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==", "dependencies": { "postcss-value-parser": "^4.2.0" }, "engines": { - "node": ">=7.6.0" + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" }, "peerDependencies": { - "postcss": "^8.4.6" + "postcss": "^8.2" } }, - "node_modules/postcss-color-functional-notation": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz", - "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==", + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "peerDependencies": { + "postcss": "^8" + } + }, + "node_modules/postcss-place": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz", + "integrity": "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==", "dependencies": { "postcss-value-parser": "^4.2.0" }, @@ -18081,11 +17346,59 @@ "postcss": "^8.2" } }, - "node_modules/postcss-color-hex-alpha": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz", - "integrity": "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==", + "node_modules/postcss-preset-env": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz", + "integrity": "sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==", "dependencies": { + "@csstools/postcss-cascade-layers": "^1.1.1", + "@csstools/postcss-color-function": "^1.1.1", + "@csstools/postcss-font-format-keywords": "^1.0.1", + "@csstools/postcss-hwb-function": "^1.0.2", + "@csstools/postcss-ic-unit": "^1.0.1", + "@csstools/postcss-is-pseudo-class": "^2.0.7", + "@csstools/postcss-nested-calc": "^1.0.0", + "@csstools/postcss-normalize-display-values": "^1.0.1", + "@csstools/postcss-oklab-function": "^1.1.1", + "@csstools/postcss-progressive-custom-properties": "^1.3.0", + "@csstools/postcss-stepped-value-functions": "^1.0.1", + "@csstools/postcss-text-decoration-shorthand": "^1.0.0", + "@csstools/postcss-trigonometric-functions": "^1.0.2", + "@csstools/postcss-unset-value": "^1.0.2", + "autoprefixer": "^10.4.13", + "browserslist": "^4.21.4", + "css-blank-pseudo": "^3.0.3", + "css-has-pseudo": "^3.0.4", + "css-prefers-color-scheme": "^6.0.3", + "cssdb": "^7.1.0", + "postcss-attribute-case-insensitive": "^5.0.2", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^4.2.4", + "postcss-color-hex-alpha": "^8.0.4", + "postcss-color-rebeccapurple": "^7.1.1", + "postcss-custom-media": "^8.0.2", + "postcss-custom-properties": "^12.1.10", + "postcss-custom-selectors": "^6.0.3", + "postcss-dir-pseudo-class": "^6.0.5", + "postcss-double-position-gradients": "^3.1.2", + "postcss-env-function": "^4.0.6", + "postcss-focus-visible": "^6.0.4", + "postcss-focus-within": "^5.0.4", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^3.0.5", + "postcss-image-set-function": "^4.0.7", + "postcss-initial": "^4.0.1", + "postcss-lab-function": "^4.2.1", + "postcss-logical": "^5.0.4", + "postcss-media-minmax": "^5.0.0", + "postcss-nesting": "^10.2.0", + "postcss-opacity-percentage": "^1.1.2", + "postcss-overflow-shorthand": "^3.0.4", + "postcss-page-break": "^3.0.4", + "postcss-place": "^7.0.5", + "postcss-pseudo-class-any-link": "^7.1.6", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^6.0.1", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -18096,15 +17409,15 @@ "url": "https://opencollective.com/csstools" }, "peerDependencies": { - "postcss": "^8.4" + "postcss": "^8.2" } }, - "node_modules/postcss-color-rebeccapurple": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz", - "integrity": "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==", + "node_modules/postcss-pseudo-class-any-link": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz", + "integrity": "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==", "dependencies": { - "postcss-value-parser": "^4.2.0" + "postcss-selector-parser": "^6.0.10" }, "engines": { "node": "^12 || ^14 || >=16" @@ -18117,15 +17430,13 @@ "postcss": "^8.2" } }, - "node_modules/postcss-colormin": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz", - "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==", + "node_modules/postcss-reduce-initial": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz", + "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==", "dependencies": { "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0", - "colord": "^2.9.1", - "postcss-value-parser": "^4.2.0" + "caniuse-api": "^3.0.0" }, "engines": { "node": "^10 || ^12 || >=14.0" @@ -18134,12 +17445,11 @@ "postcss": "^8.2.15" } }, - "node_modules/postcss-convert-values": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", - "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", + "node_modules/postcss-reduce-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", + "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", "dependencies": { - "browserslist": "^4.21.4", "postcss-value-parser": "^4.2.0" }, "engines": { @@ -18149,12 +17459,20 @@ "postcss": "^8.2.15" } }, - "node_modules/postcss-custom-media": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz", - "integrity": "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==", + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "peerDependencies": { + "postcss": "^8.0.3" + } + }, + "node_modules/postcss-selector-not": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz", + "integrity": "sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==", "dependencies": { - "postcss-value-parser": "^4.2.0" + "postcss-selector-parser": "^6.0.10" }, "engines": { "node": "^12 || ^14 || >=16" @@ -18164,2212 +17482,766 @@ "url": "https://opencollective.com/csstools" }, "peerDependencies": { - "postcss": "^8.3" + "postcss": "^8.2" } }, - "node_modules/postcss-custom-properties": { - "version": "12.1.11", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz", - "integrity": "sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-custom-selectors": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz", - "integrity": "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==", - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.3" - } - }, - "node_modules/postcss-dir-pseudo-class": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz", - "integrity": "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-discard-comments": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", - "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-duplicates": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", - "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-empty": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", - "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-discard-overridden": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", - "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-double-position-gradients": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz", - "integrity": "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-env-function": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz", - "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-flexbugs-fixes": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz", - "integrity": "sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ==", - "peerDependencies": { - "postcss": "^8.1.4" - } - }, - "node_modules/postcss-focus-visible": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", - "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", - "dependencies": { - "postcss-selector-parser": "^6.0.9" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-focus-within": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", - "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", - "dependencies": { - "postcss-selector-parser": "^6.0.9" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-font-variant": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", - "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-gap-properties": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", - "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-image-set-function": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz", - "integrity": "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-initial": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", - "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", - "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-lab-function": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz", - "integrity": "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^1.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", - "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", - "engines": { - "node": ">=14" - } - }, - "node_modules/postcss-load-config/node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", - "engines": { - "node": ">= 14" - } - }, - "node_modules/postcss-loader": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", - "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", - "dependencies": { - "cosmiconfig": "^7.0.0", - "klona": "^2.0.5", - "semver": "^7.3.5" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - } - }, - "node_modules/postcss-logical": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", - "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-media-minmax": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", - "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-merge-longhand": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", - "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^5.1.1" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-merge-rules": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz", - "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==", - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^3.1.0", - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-font-values": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", - "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-gradients": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", - "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", - "dependencies": { - "colord": "^2.9.1", - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-params": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", - "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", - "dependencies": { - "browserslist": "^4.21.4", - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-minify-selectors": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", - "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", - "dependencies": { - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", - "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.1.0.tgz", - "integrity": "sha512-SaIbK8XW+MZbd0xHPf7kdfA/3eOt7vxJ72IRecn3EzuZVLr1r0orzf0MX/pN8m+NMDoo6X/SQd8oeKqGZd8PXg==", - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-nested": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", - "dependencies": { - "postcss-selector-parser": "^6.0.11" - }, - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-nesting": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz", - "integrity": "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==", - "dependencies": { - "@csstools/selector-specificity": "^2.0.0", - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-normalize": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-10.0.1.tgz", - "integrity": "sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA==", - "dependencies": { - "@csstools/normalize.css": "*", - "postcss-browser-comments": "^4", - "sanitize.css": "*" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "browserslist": ">= 4", - "postcss": ">= 8" - } - }, - "node_modules/postcss-normalize-charset": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", - "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-display-values": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", - "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-positions": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", - "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-repeat-style": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", - "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-string": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", - "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-timing-functions": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", - "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-unicode": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", - "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", - "dependencies": { - "browserslist": "^4.21.4", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-url": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", - "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", - "dependencies": { - "normalize-url": "^6.0.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-normalize-whitespace": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", - "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-opacity-percentage": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz", - "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==", - "funding": [ - { - "type": "kofi", - "url": "https://ko-fi.com/mrcgrtz" - }, - { - "type": "liberapay", - "url": "https://liberapay.com/mrcgrtz" - } - ], - "engines": { - "node": "^12 || ^14 || >=16" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-ordered-values": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", - "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", - "dependencies": { - "cssnano-utils": "^3.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-overflow-shorthand": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz", - "integrity": "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-page-break": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", - "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", - "peerDependencies": { - "postcss": "^8" - } - }, - "node_modules/postcss-place": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz", - "integrity": "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-preset-env": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz", - "integrity": "sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==", - "dependencies": { - "@csstools/postcss-cascade-layers": "^1.1.1", - "@csstools/postcss-color-function": "^1.1.1", - "@csstools/postcss-font-format-keywords": "^1.0.1", - "@csstools/postcss-hwb-function": "^1.0.2", - "@csstools/postcss-ic-unit": "^1.0.1", - "@csstools/postcss-is-pseudo-class": "^2.0.7", - "@csstools/postcss-nested-calc": "^1.0.0", - "@csstools/postcss-normalize-display-values": "^1.0.1", - "@csstools/postcss-oklab-function": "^1.1.1", - "@csstools/postcss-progressive-custom-properties": "^1.3.0", - "@csstools/postcss-stepped-value-functions": "^1.0.1", - "@csstools/postcss-text-decoration-shorthand": "^1.0.0", - "@csstools/postcss-trigonometric-functions": "^1.0.2", - "@csstools/postcss-unset-value": "^1.0.2", - "autoprefixer": "^10.4.13", - "browserslist": "^4.21.4", - "css-blank-pseudo": "^3.0.3", - "css-has-pseudo": "^3.0.4", - "css-prefers-color-scheme": "^6.0.3", - "cssdb": "^7.1.0", - "postcss-attribute-case-insensitive": "^5.0.2", - "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^4.2.4", - "postcss-color-hex-alpha": "^8.0.4", - "postcss-color-rebeccapurple": "^7.1.1", - "postcss-custom-media": "^8.0.2", - "postcss-custom-properties": "^12.1.10", - "postcss-custom-selectors": "^6.0.3", - "postcss-dir-pseudo-class": "^6.0.5", - "postcss-double-position-gradients": "^3.1.2", - "postcss-env-function": "^4.0.6", - "postcss-focus-visible": "^6.0.4", - "postcss-focus-within": "^5.0.4", - "postcss-font-variant": "^5.0.0", - "postcss-gap-properties": "^3.0.5", - "postcss-image-set-function": "^4.0.7", - "postcss-initial": "^4.0.1", - "postcss-lab-function": "^4.2.1", - "postcss-logical": "^5.0.4", - "postcss-media-minmax": "^5.0.0", - "postcss-nesting": "^10.2.0", - "postcss-opacity-percentage": "^1.1.2", - "postcss-overflow-shorthand": "^3.0.4", - "postcss-page-break": "^3.0.4", - "postcss-place": "^7.0.5", - "postcss-pseudo-class-any-link": "^7.1.6", - "postcss-replace-overflow-wrap": "^4.0.0", - "postcss-selector-not": "^6.0.1", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-pseudo-class-any-link": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz", - "integrity": "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-reduce-initial": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz", - "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==", - "dependencies": { - "browserslist": "^4.21.4", - "caniuse-api": "^3.0.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-reduce-transforms": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", - "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-replace-overflow-wrap": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", - "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", - "peerDependencies": { - "postcss": "^8.0.3" - } - }, - "node_modules/postcss-selector-not": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz", - "integrity": "sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==", - "dependencies": { - "postcss-selector-parser": "^6.0.10" - }, - "engines": { - "node": "^12 || ^14 || >=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - "peerDependencies": { - "postcss": "^8.2" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.13", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", - "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-svgo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", - "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "svgo": "^2.7.0" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-svgo/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "engines": { - "node": ">= 10" - } - }, - "node_modules/postcss-svgo/node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/postcss-svgo/node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" - }, - "node_modules/postcss-svgo/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postcss-svgo/node_modules/svgo": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", - "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", - "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^4.1.3", - "css-tree": "^1.1.3", - "csso": "^4.2.0", - "picocolors": "^1.0.0", - "stable": "^0.1.8" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/postcss-unique-selectors": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", - "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", - "dependencies": { - "postcss-selector-parser": "^6.0.5" - }, - "engines": { - "node": "^10 || ^12 || >=14.0" - }, - "peerDependencies": { - "postcss": "^8.2.15" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/pretty-bytes": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", - "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pretty-error": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", - "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", - "dependencies": { - "lodash": "^4.17.20", - "renderkid": "^3.0.0" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/prismjs": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", - "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/promise": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", - "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", - "dependencies": { - "asap": "~2.0.6" - } - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/property-information": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", - "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" - } - }, - "node_modules/qr-code-styling": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/qr-code-styling/-/qr-code-styling-1.9.2.tgz", - "integrity": "sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg==", - "license": "MIT", - "dependencies": { - "qrcode-generator": "^1.4.4" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/qrcode-generator": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.5.2.tgz", - "integrity": "sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==", - "license": "MIT" - }, - "node_modules/qrcode.react": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", - "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", - "license": "ISC", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" - }, - "node_modules/queue": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", - "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", - "license": "MIT", - "peer": true, - "dependencies": { - "inherits": "~2.0.3" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/raf": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", - "dependencies": { - "performance-now": "^2.1.0" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-app-polyfill": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz", - "integrity": "sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w==", - "dependencies": { - "core-js": "^3.19.2", - "object-assign": "^4.1.1", - "promise": "^8.1.0", - "raf": "^3.4.1", - "regenerator-runtime": "^0.13.9", - "whatwg-fetch": "^3.6.2" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/react-app-polyfill/node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" - }, - "node_modules/react-chartjs-2": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", - "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", - "peerDependencies": { - "chart.js": "^4.1.1", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/react-dev-utils": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", - "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", - "dependencies": { - "@babel/code-frame": "^7.16.0", - "address": "^1.1.2", - "browserslist": "^4.18.1", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.3", - "detect-port-alt": "^1.1.6", - "escape-string-regexp": "^4.0.0", - "filesize": "^8.0.6", - "find-up": "^5.0.0", - "fork-ts-checker-webpack-plugin": "^6.5.0", - "global-modules": "^2.0.0", - "globby": "^11.0.4", - "gzip-size": "^6.0.0", - "immer": "^9.0.7", - "is-root": "^2.1.0", - "loader-utils": "^3.2.0", - "open": "^8.4.0", - "pkg-up": "^3.1.0", - "prompts": "^2.4.2", - "react-error-overlay": "^6.0.11", - "recursive-readdir": "^2.2.2", - "shell-quote": "^1.7.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": ">=14" + "node": ">=4" } }, - "node_modules/react-dev-utils/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/postcss-svgo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", + "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", "dependencies": { - "color-convert": "^2.0.1" + "postcss-value-parser": "^4.2.0", + "svgo": "^2.7.0" }, "engines": { - "node": ">=8" + "node": "^10 || ^12 || >=14.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/react-dev-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "node_modules/postcss-svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">= 10" } }, - "node_modules/react-dev-utils/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/postcss-svgo/node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", "dependencies": { - "color-name": "~1.1.4" + "mdn-data": "2.0.14", + "source-map": "^0.6.1" }, "engines": { - "node": ">=7.0.0" + "node": ">=8.0.0" } }, - "node_modules/react-dev-utils/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "node_modules/postcss-svgo/node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" }, - "node_modules/react-dev-utils/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/postcss-svgo/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "engines": { - "node": ">=10" + "node": ">=0.10.0" + } + }, + "node_modules/postcss-svgo/node_modules/svgo": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", + "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "stable": "^0.1.8" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" } }, - "node_modules/react-dev-utils/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/postcss-unique-selectors": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", + "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, "engines": { - "node": ">=8" + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" } }, - "node_modules/react-dev-utils/node_modules/loader-utils": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", - "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "engines": { - "node": ">= 12.13.0" + "node": ">= 0.8.0" } }, - "node_modules/react-dev-utils/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dependencies": { - "has-flag": "^4.0.0" - }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "engines": { - "node": ">=8" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-devtools-core": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-6.1.5.tgz", - "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", - "license": "MIT", - "peer": true, + "node_modules/pretty-error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", + "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", "dependencies": { - "shell-quote": "^1.6.1", - "ws": "^7" + "lodash": "^4.17.20", + "renderkid": "^3.0.0" } }, - "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" }, - "peerDependencies": { - "react": "^18.2.0" + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/react-draggable": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz", - "integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==", - "license": "MIT", - "dependencies": { - "clsx": "^2.1.1", - "prop-types": "^15.8.1" + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" }, - "peerDependencies": { - "react": ">= 16.3.0", - "react-dom": ">= 16.3.0" + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/react-error-overlay": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", - "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" - }, - "node_modules/react-floater": { - "version": "0.7.9", - "resolved": "https://registry.npmjs.org/react-floater/-/react-floater-0.7.9.tgz", - "integrity": "sha512-NXqyp9o8FAXOATOEo0ZpyaQ2KPb4cmPMXGWkx377QtJkIXHlHRAGer7ai0r0C1kG5gf+KJ6Gy+gdNIiosvSicg==", - "dependencies": { - "deepmerge": "^4.3.1", - "is-lite": "^0.8.2", - "popper.js": "^1.16.0", - "prop-types": "^15.8.1", - "tree-changes": "^0.9.1" - }, - "peerDependencies": { - "react": "15 - 18", - "react-dom": "15 - 18" + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" } }, - "node_modules/react-floater/node_modules/@gilbarbara/deep-equal": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@gilbarbara/deep-equal/-/deep-equal-0.1.2.tgz", - "integrity": "sha512-jk+qzItoEb0D0xSSmrKDDzf9sheQj/BAPxlgNxgmOaA3mxpUa6ndJLYGZKsJnIVEQSD8zcTbyILz7I0HcnBCRA==" + "node_modules/private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", + "engines": { + "node": ">= 0.6" + } }, - "node_modules/react-floater/node_modules/is-lite": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/is-lite/-/is-lite-0.8.2.tgz", - "integrity": "sha512-JZfH47qTsslwaAsqbMI3Q6HNNjUuq6Cmzzww50TdP5Esb6e1y2sK2UAaZZuzfAzpoI2AkxoPQapZdlDuP6Vlsw==" + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, - "node_modules/react-floater/node_modules/tree-changes": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/tree-changes/-/tree-changes-0.9.3.tgz", - "integrity": "sha512-vvvS+O6kEeGRzMglTKbc19ltLWNtmNt1cpBoSYLj/iEcPVvpJasemKOlxBrmZaCtDJoF+4bwv3m01UKYi8mukQ==", + "node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", "dependencies": { - "@gilbarbara/deep-equal": "^0.1.1", - "is-lite": "^0.8.2" + "asap": "~2.0.6" } }, - "node_modules/react-innertext": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/react-innertext/-/react-innertext-1.1.5.tgz", - "integrity": "sha512-PWAqdqhxhHIv80dT9znP2KvS+hfkbRovFp4zFYHFFlOoQLRiawIic81gKb3U1wEyJZgMwgs3JoLtwryASRWP3Q==", - "peerDependencies": { - "@types/react": ">=0.0.0 <=99", - "react": ">=0.0.0 <=99" + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" - }, - "node_modules/react-joyride": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/react-joyride/-/react-joyride-2.8.2.tgz", - "integrity": "sha512-2QY8HB1G0I2OT0PKMUz7gg2HAjdkG2Bqi13r0Bb1V16PAwfb9khn4wWBTOJsGsjulbAWiQ3/0YrgNUHGFmuifw==", + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dependencies": { - "@gilbarbara/deep-equal": "^0.3.1", - "deep-diff": "^1.0.2", - "deepmerge": "^4.3.1", - "is-lite": "^1.2.1", - "react-floater": "^0.7.9", - "react-innertext": "^1.1.5", - "react-is": "^16.13.1", - "scroll": "^3.0.1", - "scrollparent": "^2.1.0", - "tree-changes": "^0.11.2", - "type-fest": "^4.18.2" - }, - "peerDependencies": { - "react": "15 - 18", - "react-dom": "15 - 18" + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" } }, - "node_modules/react-joyride/node_modules/react-is": { + "node_modules/prop-types/node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "node_modules/react-joyride/node_modules/type-fest": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz", - "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==", - "engines": { - "node": ">=16" + "node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/react-konva": { - "version": "18.2.14", - "resolved": "https://registry.npmjs.org/react-konva/-/react-konva-18.2.14.tgz", - "integrity": "sha512-lBDe/5fTgquMdg1AHI0B16YZdAOvEhWMBWuo12ioyY0icdxcz9Cf12j86fsCJCHdnvjUOlZeC0f5q+siyHbD4Q==", - "funding": [ - { - "type": "patreon", - "url": "https://www.patreon.com/lavrton" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/konva" - }, - { - "type": "github", - "url": "https://github.com/sponsors/lavrton" - } - ], + "node_modules/prosemirror-changeset": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz", + "integrity": "sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==", "license": "MIT", - "peer": true, "dependencies": { - "@types/react-reconciler": "^0.28.2", - "its-fine": "^1.1.1", - "react-reconciler": "~0.29.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "konva": "^8.0.1 || ^7.2.5 || ^9.0.0 || ^10.0.0", - "react": ">=18.0.0", - "react-dom": ">=18.0.0" + "prosemirror-transform": "^1.0.0" } }, - "node_modules/react-konva/node_modules/react-reconciler": { - "version": "0.29.2", - "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", - "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", + "node_modules/prosemirror-collab": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz", + "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", "license": "MIT", - "peer": true, "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "engines": { - "node": ">=0.10.0" - }, - "peerDependencies": { - "react": "^18.3.1" + "prosemirror-state": "^1.0.0" } }, - "node_modules/react-native": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.84.1.tgz", - "integrity": "sha512-0PjxOyXRu3tZ8EobabxSukvhKje2HJbsZikR0U+pvS0pYZza2hXKjcSBiBdFN4h9D0S3v6a8kkrDK6WTRKMwzg==", + "node_modules/prosemirror-commands": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", "license": "MIT", - "peer": true, - "dependencies": { - "@jest/create-cache-key-function": "^29.7.0", - "@react-native/assets-registry": "0.84.1", - "@react-native/codegen": "0.84.1", - "@react-native/community-cli-plugin": "0.84.1", - "@react-native/gradle-plugin": "0.84.1", - "@react-native/js-polyfills": "0.84.1", - "@react-native/normalize-colors": "0.84.1", - "@react-native/virtualized-lists": "0.84.1", - "abort-controller": "^3.0.0", - "anser": "^1.4.9", - "ansi-regex": "^5.0.0", - "babel-jest": "^29.7.0", - "babel-plugin-syntax-hermes-parser": "0.32.0", - "base64-js": "^1.5.1", - "commander": "^12.0.0", - "flow-enums-runtime": "^0.0.6", - "hermes-compiler": "250829098.0.9", - "invariant": "^2.2.4", - "jest-environment-node": "^29.7.0", - "memoize-one": "^5.0.0", - "metro-runtime": "^0.83.3", - "metro-source-map": "^0.83.3", - "nullthrows": "^1.1.1", - "pretty-format": "^29.7.0", - "promise": "^8.3.0", - "react-devtools-core": "^6.1.5", - "react-refresh": "^0.14.0", - "regenerator-runtime": "^0.13.2", - "scheduler": "0.27.0", - "semver": "^7.1.3", - "stacktrace-parser": "^0.1.10", - "tinyglobby": "^0.2.15", - "whatwg-fetch": "^3.0.0", - "ws": "^7.5.10", - "yargs": "^17.6.2" - }, - "bin": { - "react-native": "cli.js" - }, - "engines": { - "node": ">= 20.19.4" - }, - "peerDependencies": { - "@types/react": "^19.1.1", - "react": "^19.2.3" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" } }, - "node_modules/react-native/node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "node_modules/prosemirror-dropcursor": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", + "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", "license": "MIT", - "peer": true, "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" } }, - "node_modules/react-native/node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "node_modules/prosemirror-gapcursor": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz", + "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==", "license": "MIT", - "peer": true, "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" } }, - "node_modules/react-native/node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "node_modules/prosemirror-history": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", "license": "MIT", - "peer": true, "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" } }, - "node_modules/react-native/node_modules/@jest/transform": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", "license": "MIT", - "peer": true, "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" } }, - "node_modules/react-native/node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", "license": "MIT", - "peer": true, "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" } }, - "node_modules/react-native/node_modules/@react-native/virtualized-lists": { - "version": "0.84.1", - "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.84.1.tgz", - "integrity": "sha512-sJoDunzhci8ZsqxlUiKoLut4xQeQcmbIgvDHGQKeBz6uEq9HgU+hCWOijMRr6sLP0slQVfBAza34Rq7IbXZZOA==", + "node_modules/prosemirror-markdown": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz", + "integrity": "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==", "license": "MIT", - "peer": true, "dependencies": { - "invariant": "^2.2.4", - "nullthrows": "^1.1.1" - }, - "engines": { - "node": ">= 20.19.4" - }, - "peerDependencies": { - "@types/react": "^19.2.0", - "react": "*", - "react-native": "*" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "@types/markdown-it": "^14.0.0", + "markdown-it": "^14.0.0", + "prosemirror-model": "^1.25.0" } }, - "node_modules/react-native/node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "node_modules/prosemirror-menu": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.3.0.tgz", + "integrity": "sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==", "license": "MIT", - "peer": true + "dependencies": { + "crelt": "^1.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } }, - "node_modules/react-native/node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "license": "BSD-3-Clause", - "peer": true, + "node_modules/prosemirror-model": { + "version": "1.25.4", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", + "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", + "license": "MIT", "dependencies": { - "type-detect": "4.0.8" + "orderedmap": "^2.0.0" } }, - "node_modules/react-native/node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "license": "BSD-3-Clause", - "peer": true, + "node_modules/prosemirror-schema-basic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz", + "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==", + "license": "MIT", "dependencies": { - "@sinonjs/commons": "^3.0.0" + "prosemirror-model": "^1.25.0" } }, - "node_modules/react-native/node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", "license": "MIT", - "peer": true, "dependencies": { - "@types/yargs-parser": "*" + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" } }, - "node_modules/react-native/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", "license": "MIT", - "peer": true, "dependencies": { - "color-convert": "^2.0.1" + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-trailing-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", + "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==", + "license": "MIT", + "dependencies": { + "@remirror/core-constants": "3.0.0", + "escape-string-regexp": "^4.0.0" }, + "peerDependencies": { + "prosemirror-model": "^1.22.1", + "prosemirror-state": "^1.4.2", + "prosemirror-view": "^1.33.8" + } + }, + "node_modules/prosemirror-trailing-node/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-native/node_modules/babel-jest": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "node_modules/prosemirror-transform": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz", + "integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==", "license": "MIT", - "peer": true, "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" + "prosemirror-model": "^1.21.0" } }, - "node_modules/react-native/node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "node_modules/prosemirror-view": { + "version": "1.41.8", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.8.tgz", + "integrity": "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==", "license": "MIT", - "peer": true, "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" + "prosemirror-model": "^1.20.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.10" } }, - "node_modules/react-native/node_modules/babel-preset-jest": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qr-code-styling": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/qr-code-styling/-/qr-code-styling-1.9.2.tgz", + "integrity": "sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg==", "license": "MIT", - "peer": true, "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" + "qrcode-generator": "^1.4.4" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, + "node": ">=18.18.0" + } + }, + "node_modules/qrcode-generator": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-1.5.2.tgz", + "integrity": "sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==", + "license": "MIT" + }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "license": "ISC", "peerDependencies": { - "@babel/core": "^7.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/react-native/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "peer": true, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "side-channel": "^1.0.4" }, "engines": { - "node": ">=10" + "node": ">=0.6" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/react-native/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "peer": true, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" + "performance-now": "^2.1.0" } }, - "node_modules/react-native/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "peer": true, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "safe-buffer": "^5.1.0" } }, - "node_modules/react-native/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT", - "peer": true - }, - "node_modules/react-native/node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "license": "MIT", - "peer": true, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "engines": { - "node": ">=18" + "node": ">= 0.6" } }, - "node_modules/react-native/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "peer": true, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, "engines": { - "node": ">=8" + "node": ">= 0.8" } }, - "node_modules/react-native/node_modules/jest-environment-node": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">= 0.8" } }, - "node_modules/react-native/node_modules/jest-haste-map": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", - "license": "MIT", - "peer": true, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" + "safer-buffer": ">= 2.1.2 < 3" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" + "node": ">=0.10.0" } }, - "node_modules/react-native/node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "license": "MIT", - "peer": true, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" + "loose-envify": "^1.1.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=0.10.0" } }, - "node_modules/react-native/node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "license": "MIT", - "peer": true, + "node_modules/react-app-polyfill": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz", + "integrity": "sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w==", "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" + "core-js": "^3.19.2", + "object-assign": "^4.1.1", + "promise": "^8.1.0", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.9", + "whatwg-fetch": "^3.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=14" } }, - "node_modules/react-native/node_modules/jest-regex-util": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", - "license": "MIT", - "peer": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node_modules/react-app-polyfill/node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, + "node_modules/react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, - "node_modules/react-native/node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "license": "MIT", - "peer": true, + "node_modules/react-dev-utils": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", + "integrity": "sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==", "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" + "@babel/code-frame": "^7.16.0", + "address": "^1.1.2", + "browserslist": "^4.18.1", + "chalk": "^4.1.2", + "cross-spawn": "^7.0.3", + "detect-port-alt": "^1.1.6", + "escape-string-regexp": "^4.0.0", + "filesize": "^8.0.6", + "find-up": "^5.0.0", + "fork-ts-checker-webpack-plugin": "^6.5.0", + "global-modules": "^2.0.0", + "globby": "^11.0.4", + "gzip-size": "^6.0.0", + "immer": "^9.0.7", + "is-root": "^2.1.0", + "loader-utils": "^3.2.0", + "open": "^8.4.0", + "pkg-up": "^3.1.0", + "prompts": "^2.4.2", + "react-error-overlay": "^6.0.11", + "recursive-readdir": "^2.2.2", + "shell-quote": "^1.7.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=14" } }, - "node_modules/react-native/node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "license": "MIT", - "peer": true, + "node_modules/react-dev-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "color-convert": "^2.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/react-native/node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "peer": true, + "node_modules/react-dev-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dependencies": { - "has-flag": "^4.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/react-native/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "license": "MIT", - "peer": true, + "node_modules/react-dev-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "color-name": "~1.1.4" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=7.0.0" } }, - "node_modules/react-native/node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "license": "MIT", - "peer": true, + "node_modules/react-dev-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/react-dev-utils/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-native/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT", - "peer": true - }, - "node_modules/react-native/node_modules/react-refresh": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", - "license": "MIT", - "peer": true, + "node_modules/react-dev-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/react-native/node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "license": "MIT", - "peer": true - }, - "node_modules/react-native/node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT", - "peer": true + "node_modules/react-dev-utils/node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "engines": { + "node": ">= 12.13.0" + } }, - "node_modules/react-native/node_modules/supports-color": { + "node_modules/react-dev-utils/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -20377,47 +18249,122 @@ "node": ">=8" } }, - "node_modules/react-native/node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "license": "ISC", - "peer": true, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "peerDependencies": { + "react": "^18.2.0" } }, - "node_modules/react-native/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "node_modules/react-draggable": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.5.0.tgz", + "integrity": "sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==", "license": "MIT", - "peer": true, "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "clsx": "^2.1.1", + "prop-types": "^15.8.1" }, - "engines": { - "node": ">=12" + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" } }, - "node_modules/react-native/node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "license": "ISC", - "peer": true, + "node_modules/react-error-overlay": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", + "integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==" + }, + "node_modules/react-floater": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/react-floater/-/react-floater-0.7.9.tgz", + "integrity": "sha512-NXqyp9o8FAXOATOEo0ZpyaQ2KPb4cmPMXGWkx377QtJkIXHlHRAGer7ai0r0C1kG5gf+KJ6Gy+gdNIiosvSicg==", + "dependencies": { + "deepmerge": "^4.3.1", + "is-lite": "^0.8.2", + "popper.js": "^1.16.0", + "prop-types": "^15.8.1", + "tree-changes": "^0.9.1" + }, + "peerDependencies": { + "react": "15 - 18", + "react-dom": "15 - 18" + } + }, + "node_modules/react-floater/node_modules/@gilbarbara/deep-equal": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@gilbarbara/deep-equal/-/deep-equal-0.1.2.tgz", + "integrity": "sha512-jk+qzItoEb0D0xSSmrKDDzf9sheQj/BAPxlgNxgmOaA3mxpUa6ndJLYGZKsJnIVEQSD8zcTbyILz7I0HcnBCRA==" + }, + "node_modules/react-floater/node_modules/is-lite": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/is-lite/-/is-lite-0.8.2.tgz", + "integrity": "sha512-JZfH47qTsslwaAsqbMI3Q6HNNjUuq6Cmzzww50TdP5Esb6e1y2sK2UAaZZuzfAzpoI2AkxoPQapZdlDuP6Vlsw==" + }, + "node_modules/react-floater/node_modules/tree-changes": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/tree-changes/-/tree-changes-0.9.3.tgz", + "integrity": "sha512-vvvS+O6kEeGRzMglTKbc19ltLWNtmNt1cpBoSYLj/iEcPVvpJasemKOlxBrmZaCtDJoF+4bwv3m01UKYi8mukQ==", + "dependencies": { + "@gilbarbara/deep-equal": "^0.1.1", + "is-lite": "^0.8.2" + } + }, + "node_modules/react-innertext": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/react-innertext/-/react-innertext-1.1.5.tgz", + "integrity": "sha512-PWAqdqhxhHIv80dT9znP2KvS+hfkbRovFp4zFYHFFlOoQLRiawIic81gKb3U1wEyJZgMwgs3JoLtwryASRWP3Q==", + "peerDependencies": { + "@types/react": ">=0.0.0 <=99", + "react": ">=0.0.0 <=99" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "node_modules/react-joyride": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/react-joyride/-/react-joyride-2.8.2.tgz", + "integrity": "sha512-2QY8HB1G0I2OT0PKMUz7gg2HAjdkG2Bqi13r0Bb1V16PAwfb9khn4wWBTOJsGsjulbAWiQ3/0YrgNUHGFmuifw==", + "dependencies": { + "@gilbarbara/deep-equal": "^0.3.1", + "deep-diff": "^1.0.2", + "deepmerge": "^4.3.1", + "is-lite": "^1.2.1", + "react-floater": "^0.7.9", + "react-innertext": "^1.1.5", + "react-is": "^16.13.1", + "scroll": "^3.0.1", + "scrollparent": "^2.1.0", + "tree-changes": "^0.11.2", + "type-fest": "^4.18.2" + }, + "peerDependencies": { + "react": "15 - 18", + "react-dom": "15 - 18" + } + }, + "node_modules/react-joyride/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-joyride/node_modules/type-fest": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.0.tgz", + "integrity": "sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==", "engines": { - "node": ">=12" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/react-optimized-image": { @@ -20597,18 +18544,6 @@ "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/react-zdog": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/react-zdog/-/react-zdog-1.2.2.tgz", - "integrity": "sha512-Ix7ALha91aOEwiHuxumCeYbARS5XNpc/w0v145oGkM6poF/CvhKJwzLhM5sEZbtrghMA+psAhOJkCTzJoseicA==", - "license": "MIT", - "peer": true, - "dependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0", - "resize-observer-polyfill": "^1.5.1" - } - }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -20893,13 +18828,6 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, - "node_modules/resize-observer-polyfill": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", - "license": "MIT", - "peer": true - }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -21129,6 +19057,12 @@ "node": ">=8" } }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, "node_modules/rsuite": { "version": "5.77.1", "resolved": "https://registry.npmjs.org/rsuite/-/rsuite-5.77.1.tgz", @@ -21504,16 +19438,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "node_modules/serialize-error": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", - "integrity": "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/serialize-javascript": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", @@ -21881,29 +19805,6 @@ "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" }, - "node_modules/stacktrace-parser": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", - "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", - "license": "MIT", - "peer": true, - "dependencies": { - "type-fest": "^0.7.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/stacktrace-parser/node_modules/type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", - "license": "(MIT OR CC0-1.0)", - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/static-eval": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", @@ -22376,16 +20277,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/suspend-react": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz", - "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "react": ">=17.0" - } - }, "node_modules/svg-parser": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", @@ -22700,13 +20591,6 @@ "node": ">=0.8" } }, - "node_modules/three": { - "version": "0.183.2", - "resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz", - "integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==", - "license": "MIT", - "peer": true - }, "node_modules/throat": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz", @@ -22717,54 +20601,6 @@ "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==" }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -23023,19 +20859,11 @@ "is-typedarray": "^1.0.0" } }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" }, "node_modules/unbox-primitive": { "version": "1.0.2", @@ -23285,13 +21113,6 @@ "node": ">= 0.8" } }, - "node_modules/vlq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", - "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", - "license": "MIT", - "peer": true - }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -23301,6 +21122,12 @@ "browser-process-hrtime": "^1.0.0" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/w3c-xmlserializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", @@ -24326,13 +22153,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zdog": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/zdog/-/zdog-1.1.3.tgz", - "integrity": "sha512-raRj6r0gPzopFm5XWBJZr/NuV4EEnT4iE+U3dp5FV5pCb588Gmm3zLIp/j9yqqcMiHH8VNQlerLTgOqL7krh6w==", - "license": "MIT", - "peer": true - }, "node_modules/zustand": { "version": "4.5.7", "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0f66b7eb..b2890e96 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,9 @@ "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", + "@tiptap/extension-placeholder": "^3.22.2", + "@tiptap/react": "^3.22.2", + "@tiptap/starter-kit": "^3.22.2", "@visx/xychart": "^3.12.0", "@xyflow/react": "^12.9.2", "axios": "^1.6.5", diff --git a/frontend/src/App.js b/frontend/src/App.js index ae01d872..09e48258 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -3,7 +3,7 @@ import './App.scss'; import './assets/fonts.css'; import './assets/Fonts/Montserrat/Montserrat.css'; import './assets/Fonts/OpenSauce/OpenSauce.css'; -import AnimatedPageWrapper from './components/AnimatedPageWrapper/AnimatedPageWrapper'; +import AnimatedPageWrapper, { StaticFullBleedPage } from './components/AnimatedPageWrapper/AnimatedPageWrapper'; import { analytics } from './services/analytics/analytics'; import { isWww, setLastTenant, setTenantConfigCache } from './config/tenantRedirect'; @@ -34,6 +34,8 @@ import ClubDash from './pages/ClubDash/ClubDash'; import PendingApprovalScreen from './pages/ClubDash/PendingApprovalScreen/PendingApprovalScreen'; import OrgDisplay from './pages/Org/OrgDisplay'; import RootDash from './pages/RootDash/RootDash'; +import AdminEventOperatorPage from './pages/RootDash/AdminEventOperatorPage'; +import AdminEventsListPage from './pages/RootDash/AdminEventsListPage'; import OrgManagement from './pages/FeatureAdmin/OrgManagement/Atlas'; import ForgotPassword from './pages/ForgotPassword'; import ResetPassword from './pages/ResetPassword'; @@ -61,6 +63,7 @@ import EventPage from './pages/EventPage/EventPage'; import SubSidebarExample from './components/Dashboard/SubSidebarExample'; import RebrandingNotice from './components/RebrandingNotice/RebrandingNotice'; import DevTenantSelector from './components/DevTenantSelector/DevTenantSelector'; +import CommunityOrganizerFeatureAdminRedirect from './components/CommunityOrganizerFeatureAdminRedirect/CommunityOrganizerFeatureAdminRedirect'; import Beacon from './pages/FeatureAdmin/Beacon/Beacon'; import Compass from './pages/FeatureAdmin/Compass/Compass'; import Atlas from './pages/FeatureAdmin/Atlas/Atlas'; @@ -254,12 +257,14 @@ function App() { }/> - }/> - }/> + }/> + }/> {/* features under development */} }> {/* }/> */} }/> + }/> + }/>
}/> }/> }/> @@ -275,9 +280,9 @@ function App() { {/* oie routes */} }> }/> - }/> - }/> - }/> + }/> + }/> + }/> }/> {/* Mockup only — Admin Outreach wireframes; no auth, no prod */} diff --git a/frontend/src/AuthContext.js b/frontend/src/AuthContext.js index 97f4a6bd..ba3814d4 100644 --- a/frontend/src/AuthContext.js +++ b/frontend/src/AuthContext.js @@ -13,6 +13,47 @@ https://incongruous-reply-44a.notion.site/Frontend-AuthProvider-Component-AuthCo export const AuthContext = createContext(); +function hasNameValue(userLike) { + return String(userLike?.name || '').trim().length > 0; +} + +function hasUsernameValue(userLike) { + return String(userLike?.username || '').trim().length >= 3; +} + +function hasMissingOnboardingSteps(user, onboardingConfig) { + if (!user || !onboardingConfig || onboardingConfig.enabled !== true) return false; + const completed = new Set(Array.isArray(user.onboardingCompletedSteps) ? user.onboardingCompletedSteps : []); + const responses = user.onboardingResponses && typeof user.onboardingResponses === 'object' + ? user.onboardingResponses + : {}; + + if (onboardingConfig.collectName !== false && !hasNameValue(user)) return true; + if (!hasUsernameValue(user)) return true; + if (onboardingConfig.collectInterests !== false) { + const enforceMin = onboardingConfig.enforceMinInterests !== false; + const tags = Array.isArray(user.tags) ? user.tags : []; + if (enforceMin) { + const min = Number(onboardingConfig.minInterests ?? 1); + if (tags.length < min) return true; + } + } + + const customSteps = Array.isArray(onboardingConfig.customSteps) ? onboardingConfig.customSteps : []; + return customSteps.some((step) => { + const stepId = String(step?.id || '').trim(); + if (!stepId) return false; + const response = responses[stepId]; + const hasResponse = Array.isArray(response) + ? response.length > 0 + : String(response || '').trim().length > 0; + // Show newly added steps even if optional until acknowledged once. + if (!completed.has(stepId) && !hasResponse) return true; + if (step.required && !hasResponse) return true; + return false; + }); +} + export const AuthProvider = ({ children }) => { const [isAuthenticated, setIsAuthenticated] = useState(false); const [isAuthenticating, setIsAuthenticating] = useState(true); // [1 @@ -137,6 +178,23 @@ export const AuthProvider = ({ children }) => { } setIsAuthenticated(true); getCheckedIn(); + + // Root-configurable onboarding gate for all users missing newly required/unseen steps. + try { + const onboarding = await apiRequest('/org-management/onboarding-config', null, { method: 'GET' }); + const isEnabled = onboarding?.success && onboarding?.data?.enabled === true; + const isOnboardPath = window.location.pathname.startsWith('/onboard'); + if (isEnabled && hasMissingOnboardingSteps(u, onboarding?.data) && !isOnboardPath) { + const nextPath = window.location.pathname + (window.location.search || ''); + const next = nextPath && nextPath !== '/onboard' + ? `?next=${encodeURIComponent(nextPath)}` + : ''; + window.location.href = `/onboard${next}`; + return response; + } + } catch (_) { + // If config lookup fails, don't block login. + } } setIsAuthenticating(false); } else { diff --git a/frontend/src/components/AnimatedPageWrapper/AnimatedPageWrapper.jsx b/frontend/src/components/AnimatedPageWrapper/AnimatedPageWrapper.jsx index 41160612..49a1f6c8 100644 --- a/frontend/src/components/AnimatedPageWrapper/AnimatedPageWrapper.jsx +++ b/frontend/src/components/AnimatedPageWrapper/AnimatedPageWrapper.jsx @@ -1,6 +1,19 @@ import { motion, AnimatePresence } from 'framer-motion'; import { useLocation } from 'react-router-dom'; +/** + * Same full-bleed shell as the motion wrapper below, without Framer Motion. + * Club dashboard + calendar subtree stressed the compositor and triggered Chrome + * renderer crashes (Aw, Snap / error 4) when combined with page enter/exit animations. + */ +export function StaticFullBleedPage({ children }) { + return ( +
+ {children} +
+ ); +} + const variants = { initial: { opacity: 0, y: 10 }, animate: { opacity: 1, y: 0, transition: { duration: 0.4 } }, diff --git a/frontend/src/components/ApprovalPreview/ApprovalPreview.jsx b/frontend/src/components/ApprovalPreview/ApprovalPreview.jsx index 409265aa..618c6268 100644 --- a/frontend/src/components/ApprovalPreview/ApprovalPreview.jsx +++ b/frontend/src/components/ApprovalPreview/ApprovalPreview.jsx @@ -2,19 +2,35 @@ import React, { useState, useEffect } from 'react'; import { Icon } from '@iconify-icon/react/dist/iconify.mjs'; import { useFetch } from '../../hooks/useFetch'; import postRequest from '../../utils/postRequest'; +import { extractResourceId } from '../../pages/CreateEvent/shared/resourcePreflight'; import './ApprovalPreview.scss'; -const ApprovalPreview = ({ formData, hideUnlessRequired = false }) => { +const ApprovalPreview = ({ + formData, + hideUnlessRequired = false, + hideWhenOnlyNotifications = false +}) => { const [previewData, setPreviewData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { - // Only fetch if we have minimum required data - if (formData && formData.location && formData.type && formData.start_time) { + // Best-effort preview: start as soon as event type exists, refine as more fields are filled. + if (formData && formData.type) { fetchApprovalPreview(); + } else { + setPreviewData(null); + setError(null); } - }, [formData?.location, formData?.type, formData?.start_time, formData?.end_time, formData?.expectedAttendance]); + }, [ + formData?.location, + formData?.type, + formData?.start_time, + formData?.end_time, + formData?.expectedAttendance, + formData?.classroom_id, + formData?.classroomId + ]); const fetchApprovalPreview = async () => { setLoading(true); @@ -22,6 +38,7 @@ const ApprovalPreview = ({ formData, hideUnlessRequired = false }) => { try { // Prepare event data for preview + const resourceId = extractResourceId(formData); const eventPreview = { location: formData.location, type: formData.type, @@ -31,7 +48,9 @@ const ApprovalPreview = ({ formData, hideUnlessRequired = false }) => { name: formData.name, description: formData.description, visibility: formData.visibility, - customFields: formData.customFields || {} + customFields: formData.customFields || {}, + classroom_id: resourceId, + resourceId }; const response = await postRequest('/preview-approvals', eventPreview); @@ -89,8 +108,15 @@ const ApprovalPreview = ({ formData, hideUnlessRequired = false }) => { if (hideUnlessRequired) { if (loading || error || !previewData) return null; - const { total } = previewData; + const { + total = 0, + approvals = [], + acknowledgements = [] + } = previewData; if (total === 0) return null; + if (hideWhenOnlyNotifications && approvals.length === 0 && acknowledgements.length === 0) { + return null; + } } else { if (loading) { return ( diff --git a/frontend/src/components/BudgetWorkflow/BudgetAuditTimeline.jsx b/frontend/src/components/BudgetWorkflow/BudgetAuditTimeline.jsx new file mode 100644 index 00000000..f0cfccb6 --- /dev/null +++ b/frontend/src/components/BudgetWorkflow/BudgetAuditTimeline.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import './BudgetAuditTimeline.scss'; + +const ACTION_LABELS = { + draft_created: 'Draft created', + submitted: 'Submitted for review', + officer_stage_approved: 'Officer stage approved', + officer_approved: 'Officer approved', + approved: 'Approved', + platform_stage_approved: 'Finance office stage approved', + workflow_completed_approved: 'Fully approved', + rejected: 'Rejected', + revision_requested: 'Revision requested', + resumed_after_revision: 'Returned to draft after revision' +}; + +function formatWhen(at) { + if (!at) return ''; + try { + return new Date(at).toLocaleString(); + } catch { + return ''; + } +} + +export default function BudgetAuditTimeline({ entries, title = 'Activity log', className = '' }) { + const list = Array.isArray(entries) ? [...entries].reverse() : []; + + if (!list.length) { + return ( +
+

{title}

+

No recorded activity yet.

+
+ ); + } + + return ( +
+

{title}

+
    + {list.map((e, i) => ( +
  1. +
    + + {e.actor === 'platform' ? 'Finance office' : e.actor === 'system' ? 'System' : 'Organization'} + + +
    +

    {ACTION_LABELS[e.action] || e.action}

    + {(e.fromStatus || e.toStatus) && ( +

    + {e.fromStatus && e.toStatus && e.fromStatus !== e.toStatus + ? `${e.fromStatus} → ${e.toStatus}` + : e.toStatus || e.fromStatus} +

    + )} + {e.stageKey ?

    Stage: {e.stageKey}

    : null} + {e.message ?

    {e.message}

    : null} +
  2. + ))} +
+
+ ); +} diff --git a/frontend/src/components/BudgetWorkflow/BudgetAuditTimeline.scss b/frontend/src/components/BudgetWorkflow/BudgetAuditTimeline.scss new file mode 100644 index 00000000..191173bf --- /dev/null +++ b/frontend/src/components/BudgetWorkflow/BudgetAuditTimeline.scss @@ -0,0 +1,108 @@ +.budget-audit { + &__title { + margin: 0 0 12px; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--light-text); + } + + &__empty { + margin: 0; + font-size: 13px; + color: var(--light-text); + } + + &__list { + list-style: none; + margin: 0; + padding: 0; + border-left: 2px solid var(--lightborder); + padding-left: 14px; + } + + &__item { + position: relative; + padding-bottom: 16px; + + &::before { + content: ''; + position: absolute; + left: -17px; + top: 4px; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--primary-color); + border: 2px solid var(--background); + } + + &:last-child { + padding-bottom: 0; + } + } + + &__row { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 8px 12px; + margin-bottom: 4px; + } + + &__actor { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + padding: 2px 8px; + border-radius: 6px; + + &--org { + background: rgba(77, 170, 87, 0.15); + color: #2d7a36; + } + + &--platform { + background: rgba(33, 99, 168, 0.12); + color: #215a9a; + } + + &--system { + background: var(--lighter); + color: var(--light-text); + } + } + + &__time { + font-size: 12px; + color: var(--light-text); + } + + &__action { + margin: 0; + font-size: 14px; + font-weight: 600; + color: var(--text); + } + + &__statuses, + &__stage { + margin: 2px 0 0; + font-size: 12px; + color: var(--light-text); + } + + &__message { + margin: 8px 0 0; + font-size: 13px; + line-height: 1.45; + color: var(--text); + padding: 10px 12px; + background: var(--lighter); + border-radius: 8px; + border: 1px solid var(--lightborder); + white-space: pre-wrap; + } +} diff --git a/frontend/src/components/BudgetWorkflow/BudgetMessageModal.scss b/frontend/src/components/BudgetWorkflow/BudgetMessageModal.scss new file mode 100644 index 00000000..1a0acbcd --- /dev/null +++ b/frontend/src/components/BudgetWorkflow/BudgetMessageModal.scss @@ -0,0 +1,94 @@ +.budget-msg-modal-overlay { + position: fixed; + inset: 0; + z-index: 10100; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + box-sizing: border-box; +} + +.budget-msg-modal { + width: min(480px, 100%); + background: var(--background); + border-radius: 12px; + border: 1px solid var(--lightborder); + padding: 22px 24px 20px; + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.2); + + &--in-popup { + border: none; + box-shadow: none; + padding: 8px 4px 4px; + width: 100%; + max-width: 440px; + box-sizing: border-box; + } + + h2 { + margin: 0 0 8px; + font-size: 1.15rem; + font-weight: 700; + color: var(--text); + } + + &__desc { + margin: 0 0 14px; + font-size: 14px; + line-height: 1.45; + color: var(--light-text); + } + + &__input { + width: 100%; + box-sizing: border-box; + padding: 10px 12px; + border-radius: 8px; + border: 1px solid var(--lightborder); + background: var(--light); + color: var(--text); + font-family: inherit; + font-size: 14px; + resize: vertical; + min-height: 100px; + + &--single { + min-height: unset; + } + } + + &__actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 18px; + } + + &__btn { + padding: 9px 16px; + border-radius: 8px; + font-weight: 600; + font-size: 14px; + font-family: inherit; + cursor: pointer; + border: none; + + &--ghost { + background: transparent; + color: var(--light-text); + border: 1px solid var(--lightborder); + } + + &--primary { + background: var(--primary-color); + color: var(--background); + } + + &:disabled { + opacity: 0.45; + cursor: not-allowed; + } + } +} diff --git a/frontend/src/components/BudgetWorkflow/BudgetStageMessagePopupBody.jsx b/frontend/src/components/BudgetWorkflow/BudgetStageMessagePopupBody.jsx new file mode 100644 index 00000000..3889d529 --- /dev/null +++ b/frontend/src/components/BudgetWorkflow/BudgetStageMessagePopupBody.jsx @@ -0,0 +1,74 @@ +import React, { useState, useEffect } from 'react'; +import './BudgetMessageModal.scss'; + +/** + * Content for {@link Popup}: cloneElement injects `handleClose`. + * Omit overlay styles; Popup supplies the shell. + */ +export default function BudgetStageMessagePopupBody({ + handleClose, + title, + description, + placeholder = '', + submitLabel = 'Submit', + cancelLabel = 'Cancel', + requireNonEmpty = false, + multiline = true, + rows = 5, + onSubmit +}) { + const [text, setText] = useState(''); + + useEffect(() => { + setText(''); + }, [title]); + + const submit = () => { + const t = text.trim(); + if (requireNonEmpty && !t) return; + onSubmit(t); + }; + + return ( +
e.stopPropagation()} + > +

{title}

+ {description &&

{description}

} + {multiline ? ( +