diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc961d7..08989f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,8 +11,6 @@ jobs: steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 - with: - version: latest - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 diff --git a/CLAUDE.md b/CLAUDE.md index 40e968f..dc99fad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,16 +1,20 @@ -# Nicky - Journaling App +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview -Nicky is a React Native journaling app built with Expo and Expo Router. It features native iOS UI via `@expo/ui/swift-ui` (SwiftUI components), native tab navigation, and file-based routing. +Nicky is a React Native journaling app built with Expo and Expo Router. It features native iOS UI via `@expo/ui/swift-ui` (SwiftUI components), native tab navigation, and SQLite persistence via Drizzle ORM. ## Development Commands ```bash -pnpm expo run:ios # Build and run on iOS simulator -pnpm expo start # Start Expo dev server -pnpm lint # Run ESLint -pnpm lint-fix # Run ESLint with auto-fix +pnpm expo run:ios # Build and run on iOS simulator +pnpm expo start --clear # Start Expo dev server (clear cache) +pnpm lint # Run ESLint +pnpm lint-fix # Run ESLint with auto-fix +pnpm typecheck # TypeScript type check +pnpm drizzle-kit generate # Generate migration files from schema ``` ## Commit Convention @@ -30,7 +34,7 @@ chore: tooling / config changes ``` src/app/ - _layout.tsx # NativeTabs (root) — tab bar always visible + _layout.tsx # Root — wraps everything in DrizzleProvider + NativeTabs (journal)/ _layout.tsx # Stack — scoped to journal tab index.tsx # Journal list / @@ -45,46 +49,46 @@ src/app/ **Key rule:** `NativeTabs` is the root navigator; `Stack` lives inside `(journal)` group. This keeps the tab bar visible when pushing screens. -### Directory Structure +### Database Layer -``` -src/ - app/ # Routes (Expo Router file-based) - components/ - app-tabs.tsx # NativeTabs definition - journal/ # Journal-related components - journal-view.tsx # Grid list of journal cards - journal-card.tsx # Tappable gradient card - journal-create-view.tsx - entry/ # Entry-related components - entry-list-view.tsx # List grouped by month - entry-row.tsx # Date + title + preview row - mocks/ - journals.ts # JournalObj type + JOURNALS array - entries.ts # Entry type + ENTRIES array - constants/ - hooks/ -``` +Drizzle ORM + expo-sqlite. Schema is split by domain in `src/db/schemas/`: + +| File | Tables | +|---|---| +| `journals.ts` | `journals` | +| `fields.ts` | `fields` (field definitions per journal) | +| `entries.ts` | `entries`, `entry_values` | + +`src/db/schemas/index.ts` re-exports all schemas. `src/components/drizzle-provider.tsx` opens the DB, runs migrations via `useMigrations`, and exports `db`. + +**Adding a schema change:** edit the relevant schema file → `pnpm drizzle-kit generate` → commit the generated files in `drizzle/`. + +**Drizzle config notes:** +- Schema files must not import React Native packages (`expo-crypto`, `expo-symbols` runtime imports) — drizzle-kit runs in Node.js. Use `import type` for RN types. +- `$defaultFn` with `Crypto.randomUUID()` cannot be used in schema — generate IDs at the application layer instead. ### Key Technologies | Package | Usage | |---|---| | `expo-router` | File-based routing, `useRouter`, `useLocalSearchParams` | -| `@expo/ui/swift-ui` | SwiftUI components: `Host`, `ZStack`, `VStack`, `Grid`, `ScrollView`, `List`, `Section`, `Button`, `Image`, `Text`, `RoundedRectangle` | -| `@expo/ui/swift-ui/modifiers` | `frame`, `padding`, `font`, `foregroundStyle`, `onTapGesture`, `clipShape`, `lineLimit`, `headerProminence`, `listStyle` | +| `@expo/ui/swift-ui` | SwiftUI components: `Host`, `ZStack`, `VStack`, `Grid`, `ScrollView`, `List`, `Section`, `Button`, `Image`, `Text`, `RoundedRectangle`, `ColorPicker`, `BottomSheet` | +| `@expo/ui/swift-ui/modifiers` | `frame`, `padding`, `foregroundStyle`, `onTapGesture`, `listStyle`, `presentationDetents`, `environment`, `fixedSize` | | `expo-router/unstable-native-tabs` | `NativeTabs` — iOS native tab bar | -| `expo-symbols` | `SymbolView` — SF Symbols in RN header components | +| `expo-symbols` | `SymbolView` — SF Symbols in RN (non-SwiftUI) header components | +| `expo-sqlite` + `drizzle-orm` | Local SQLite persistence | +| `expo-crypto` | `Crypto.randomUUID()` for ID generation at the app layer | | `PlatformColor` | Adaptive system colors: `"label"`, `"systemBackground"`, `"systemIndigo"` | ### SwiftUI Component Rules - Always wrap SwiftUI components in `` with `useViewportSizeMeasurement` - Use `onTapGesture` modifier for taps — `onPress` prop does NOT work on layout components -- Gradients: `RoundedRectangle` + `foregroundStyle({ type: "linearGradient", ... })` + `clipShape` on parent `ZStack` -- Adaptive colors: use `PlatformColor("label")` directly — no need for `useColorScheme` -- Secondary text: `foregroundStyle({ type: "hierarchical", style: "secondary" })` +- `Button` inside `List` gets a blue tint by default — use `foregroundStyle({ type: "hierarchical", style: "primary" })` to keep the tap highlight without the blue color - `List` manages its own scrolling — never nest it inside `ScrollView` +- Adaptive colors: use `PlatformColor("label")` directly — no need for `useColorScheme` +- `fixedSize()` on a component prevents it from stretching in an HStack, allowing siblings to fill remaining space +- Gradients: `RoundedRectangle` + `foregroundStyle({ type: "linearGradient", ... })` + `clipShape` on parent `ZStack` ### Naming Conventions @@ -96,8 +100,7 @@ src/ ## Code Style - **Import order:** external → internal (`@/`) → relative; always separated by newlines -- **Unused imports:** auto-enforced by ESLint (`eslint-plugin-unused-imports`) - **Path alias:** `@/*` → `src/*` - **TypeScript:** strict mode enabled - **Formatter:** Prettier (enforced via ESLint) -- **React Compiler:** enabled (`reactCompiler: true` in app.json) +- **React Compiler:** enabled — do not manually add `useMemo`/`useCallback` unless there is a specific reason diff --git a/app.json b/app.json index 43fb25d..fe77076 100644 --- a/app.json +++ b/app.json @@ -35,7 +35,8 @@ "imageWidth": 76 } } - ] + ], + "expo-sqlite" ], "experiments": { "typedRoutes": true, diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..6ff01f4 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,8 @@ +/** @type {import('@babel/core').TransformOptions} */ +module.exports = function (api) { + api.cache(true); + return { + presets: ["babel-preset-expo"], + plugins: [["inline-import", { extensions: [".sql"] }]], + }; +}; diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..5a56b52 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,8 @@ +import type { Config } from "drizzle-kit"; + +export default { + dialect: "sqlite", + driver: "expo", + schema: "./src/db/schemas", + out: "./drizzle", +} satisfies Config; diff --git a/drizzle/0000_young_bulldozer.sql b/drizzle/0000_young_bulldozer.sql new file mode 100644 index 0000000..b92f1f3 --- /dev/null +++ b/drizzle/0000_young_bulldozer.sql @@ -0,0 +1,36 @@ +CREATE TABLE `entries` ( + `id` text PRIMARY KEY NOT NULL, + `journalId` text NOT NULL, + `bookmark` integer DEFAULT false NOT NULL, + `createdAt` integer NOT NULL, + `updatedAt` integer NOT NULL, + FOREIGN KEY (`journalId`) REFERENCES `journals`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `entry_values` ( + `id` text PRIMARY KEY NOT NULL, + `entryId` text NOT NULL, + `fieldId` text NOT NULL, + `value` text, + FOREIGN KEY (`entryId`) REFERENCES `entries`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`fieldId`) REFERENCES `fields`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE UNIQUE INDEX `entry_field_unique` ON `entry_values` (`entryId`,`fieldId`);--> statement-breakpoint +CREATE TABLE `fields` ( + `id` text PRIMARY KEY NOT NULL, + `journalId` text NOT NULL, + `type` text NOT NULL, + `label` text NOT NULL, + `sortOrder` integer NOT NULL, + FOREIGN KEY (`journalId`) REFERENCES `journals`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `journals` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `icon` text NOT NULL, + `color` text NOT NULL, + `createdAt` integer NOT NULL, + `updatedAt` integer NOT NULL +); diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..2f87d01 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,263 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "3d118337-e09e-4635-9ca5-d9d5a5c94bf2", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "entries": { + "name": "entries", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "journalId": { + "name": "journalId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bookmark": { + "name": "bookmark", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "entries_journalId_journals_id_fk": { + "name": "entries_journalId_journals_id_fk", + "tableFrom": "entries", + "tableTo": "journals", + "columnsFrom": [ + "journalId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "entry_values": { + "name": "entry_values", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "entryId": { + "name": "entryId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fieldId": { + "name": "fieldId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "entry_field_unique": { + "name": "entry_field_unique", + "columns": [ + "entryId", + "fieldId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "entry_values_entryId_entries_id_fk": { + "name": "entry_values_entryId_entries_id_fk", + "tableFrom": "entry_values", + "tableTo": "entries", + "columnsFrom": [ + "entryId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "entry_values_fieldId_fields_id_fk": { + "name": "entry_values_fieldId_fields_id_fk", + "tableFrom": "entry_values", + "tableTo": "fields", + "columnsFrom": [ + "fieldId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "fields": { + "name": "fields", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "journalId": { + "name": "journalId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sortOrder": { + "name": "sortOrder", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "fields_journalId_journals_id_fk": { + "name": "fields_journalId_journals_id_fk", + "tableFrom": "fields", + "tableTo": "journals", + "columnsFrom": [ + "journalId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "journals": { + "name": "journals", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..9af7705 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1777997392852, + "tag": "0000_young_bulldozer", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/drizzle/migrations.js b/drizzle/migrations.js new file mode 100644 index 0000000..dbad105 --- /dev/null +++ b/drizzle/migrations.js @@ -0,0 +1,12 @@ +// This file is required for Expo/React Native SQLite migrations - https://orm.drizzle.team/quick-sqlite/expo + +import journal from './meta/_journal.json'; +import m0000 from './0000_young_bulldozer.sql'; + + export default { + journal, + migrations: { + m0000 + } + } + \ No newline at end of file diff --git a/lefthook.yml b/lefthook.yml index 90a1f91..7a46edf 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -10,6 +10,8 @@ pre-commit: lint-and-format-check: glob: "*.{ts,tsx,js,jsx}" run: pnpm run lint + typecheck: + run: pnpm run typecheck commit-msg: commands: diff --git a/package.json b/package.json index 320cd68..b03d562 100644 --- a/package.json +++ b/package.json @@ -17,18 +17,23 @@ "@react-navigation/bottom-tabs": "^7.15.5", "@react-navigation/elements": "^2.9.10", "@react-navigation/native": "^7.1.33", + "babel-plugin-inline-import": "^3.0.0", + "drizzle-kit": "^0.31.10", + "drizzle-orm": "^0.45.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-unused-imports": "^4.4.1", "expo": "~55.0.18", "expo-constants": "~55.0.15", "expo-crypto": "~55.0.14", "expo-device": "~55.0.15", + "expo-drizzle-studio-plugin": "^0.2.1", "expo-font": "~55.0.6", "expo-glass-effect": "~55.0.10", "expo-image": "~55.0.9", "expo-linking": "~55.0.14", "expo-router": "~55.0.13", "expo-splash-screen": "~55.0.19", + "expo-sqlite": "~55.0.15", "expo-status-bar": "~55.0.5", "expo-symbols": "~55.0.7", "expo-system-ui": "~55.0.16", @@ -52,5 +57,12 @@ "prettier": "^3.8.3", "typescript": "~5.9.2" }, - "private": true -} + "packageManager": "pnpm@11.0.0", + "private": true, + "pnpm": { + "onlyBuiltDependencies": [ + "esbuild", + "unrs-resolver" + ] + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 85accdd..97f8077 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,15 @@ importers: '@react-navigation/native': specifier: ^7.1.33 version: 7.2.2(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + babel-plugin-inline-import: + specifier: ^3.0.0 + version: 3.0.0 + drizzle-kit: + specifier: ^0.31.10 + version: 0.31.10 + drizzle-orm: + specifier: ^0.45.2 + version: 0.45.2(expo-sqlite@55.0.15(expo@55.0.18(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)) eslint-plugin-import: specifier: ^2.32.0 version: 2.32.0(@typescript-eslint/parser@8.59.1(eslint@9.39.4)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.59.1(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4))(eslint@9.39.4))(eslint@9.39.4) @@ -38,6 +47,9 @@ importers: expo-device: specifier: ~55.0.15 version: 55.0.15(expo@55.0.18(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)) + expo-drizzle-studio-plugin: + specifier: ^0.2.1 + version: 0.2.1(expo-sqlite@55.0.15(expo@55.0.18(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(expo@55.0.18(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)) expo-font: specifier: ~55.0.6 version: 55.0.6(expo@55.0.18(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) @@ -56,6 +68,9 @@ importers: expo-splash-screen: specifier: ~55.0.19 version: 55.0.19(expo@55.0.18(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(typescript@5.9.3) + expo-sqlite: + specifier: ~55.0.15 + version: 55.0.15(expo@55.0.18(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) expo-status-bar: specifier: ~55.0.5 version: 55.0.5(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) @@ -646,18 +661,473 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@egjs/hammerjs@2.0.17': resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} engines: {node: '>=0.8.0'} - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} @@ -1646,12 +2116,18 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + await-lock@2.2.2: + resolution: {integrity: sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==} + babel-jest@29.7.0: resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: '@babel/core': ^7.8.0 + babel-plugin-inline-import@3.0.0: + resolution: {integrity: sha512-thnykl4FMb8QjMjVCuZoUmAM7r2mnTn5qJwrryCvDv6rugbJlTHZMctdjDtEgD0WBAXJOLJSGXN3loooEwx7UQ==} + babel-plugin-istanbul@6.1.1: resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} engines: {node: '>=8'} @@ -1995,6 +2471,102 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + drizzle-kit@0.31.10: + resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==} + hasBin: true + + drizzle-orm@0.45.2: + resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -2051,6 +2623,21 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2251,6 +2838,12 @@ packages: peerDependencies: expo: '*' + expo-drizzle-studio-plugin@0.2.1: + resolution: {integrity: sha512-AjMC7SOutMAv/MkeJSp/26gnHLAA17SG8EtsjIKW6tDcBkq4plDVhjMLusoGe9f0vzYK8KrOvWjEWZ/R+KvIGg==} + peerDependencies: + expo: '>=53.0.5' + expo-sqlite: '>=15.2.9' + expo-file-system@55.0.17: resolution: {integrity: sha512-d27K1cagUOt2BwxwPka9KW8Znu5kN1tnairozCzzCRZviZFtWnBxwFuJ3KU6MAbav/9UhSMkp5Ve/oZ+SR0UgQ==} peerDependencies: @@ -2352,6 +2945,13 @@ packages: peerDependencies: expo: '*' + expo-sqlite@55.0.15: + resolution: {integrity: sha512-vxE5fs6l953QSIyievQ8TuSstj62eC7zUREjNzbUOwRWaHGGnhnlPJM1HLoTIv+oIt3+b1m7k2fmcDGkpK5t3w==} + peerDependencies: + expo: '*' + react: '*' + react-native: '*' + expo-status-bar@55.0.5: resolution: {integrity: sha512-qb0c3rJO2b7CC0gUVGi1JYp92oLenWdYGyk8l4YQs6U+uaXUTPv6aaFa3KkT2HON10re3AxxPNJci8rsz6kPxg==} peerDependencies: @@ -3329,6 +3929,9 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-extra@1.0.3: + resolution: {integrity: sha512-vYm3+GCkjUlT1rDvZnDVhNLXIRvwFPaN8ebHAFcuMJM/H0RBOPD7JrcldiNLd9AS3dhAyUHLa4Hny5wp1A+Ffw==} + path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -3589,6 +4192,9 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-resolve@0.0.2: + resolution: {integrity: sha512-eafQVaxdQsWUB8HybwognkdcIdKdQdQBwTxH48FuE6WI0owZGKp63QYr1MRp73PoX0AcyB7MDapZThYUY8FD0A==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -3925,6 +4531,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -4146,6 +4757,9 @@ packages: utf-8-validate: optional: true + x-path@0.0.2: + resolution: {integrity: sha512-zQ4WFI0XfJN1uEkkrB19Y4TuXOlHqKSxUJo0Yt+axPjRm8tCG6SJ6+Wo3/+Kjg4c2c8IvBXuJ0uYoshxNn4qMw==} + xcode@3.0.1: resolution: {integrity: sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==} engines: {node: '>=10.0.0'} @@ -4834,6 +5448,8 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@drizzle-team/brocli@0.10.2': {} + '@egjs/hammerjs@2.0.17': dependencies: '@types/hammerjs': 2.0.46 @@ -4854,6 +5470,238 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.14.0 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': dependencies: eslint: 9.39.4 @@ -6125,6 +6973,8 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + await-lock@2.2.2: {} + babel-jest@29.7.0(@babel/core@7.29.0): dependencies: '@babel/core': 7.29.0 @@ -6138,6 +6988,10 @@ snapshots: transitivePeerDependencies: - supports-color + babel-plugin-inline-import@3.0.0: + dependencies: + require-resolve: 0.0.2 + babel-plugin-istanbul@6.1.1: dependencies: '@babel/helper-plugin-utils': 7.28.6 @@ -6535,6 +7389,17 @@ snapshots: dependencies: esutils: 2.0.3 + drizzle-kit@0.31.10: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.12 + tsx: 4.21.0 + + drizzle-orm@0.45.2(expo-sqlite@55.0.15(expo@55.0.18(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)): + optionalDependencies: + expo-sqlite: 55.0.15(expo@55.0.18(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -6656,6 +7521,89 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -6902,6 +7850,11 @@ snapshots: expo: 55.0.18(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3) ua-parser-js: 0.7.41 + expo-drizzle-studio-plugin@0.2.1(expo-sqlite@55.0.15(expo@55.0.18(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(expo@55.0.18(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)): + dependencies: + expo: 55.0.18(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3) + expo-sqlite: 55.0.15(expo@55.0.18(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0) + expo-file-system@55.0.17(expo@55.0.18(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0)): dependencies: expo: 55.0.18(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3) @@ -7020,6 +7973,13 @@ snapshots: - supports-color - typescript + expo-sqlite@55.0.15(expo@55.0.18(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): + dependencies: + await-lock: 2.2.2 + expo: 55.0.18(@babel/core@7.29.0)(@expo/dom-webview@55.0.5)(@expo/metro-runtime@55.0.10)(expo-router@55.0.13)(react-dom@19.2.0(react@19.2.0))(react-native-worklets@0.7.4(@babel/core@7.29.0)(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3) + react: 19.2.0 + react-native: 0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0) + expo-status-bar@55.0.5(react-native@0.83.6(@babel/core@7.29.0)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0): dependencies: react: 19.2.0 @@ -8127,6 +9087,8 @@ snapshots: path-exists@4.0.0: {} + path-extra@1.0.3: {} + path-is-absolute@1.0.1: {} path-key@3.1.1: {} @@ -8437,6 +9399,10 @@ snapshots: require-directory@2.1.1: {} + require-resolve@0.0.2: + dependencies: + x-path: 0.0.2 + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -8797,6 +9763,13 @@ snapshots: tslib@2.8.1: {} + tsx@4.21.0: + dependencies: + esbuild: 0.27.7 + get-tsconfig: 4.14.0 + optionalDependencies: + fsevents: 2.3.3 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -9029,6 +10002,10 @@ snapshots: ws@8.20.0: {} + x-path@0.0.2: + dependencies: + path-extra: 1.0.3 + xcode@3.0.1: dependencies: simple-plist: 1.3.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..6c458ab --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +allowBuilds: + esbuild: true + unrs-resolver: true diff --git a/src/app/(journal)/[id].tsx b/src/app/(journal)/[id].tsx index a8f51d0..4d2ecc6 100644 --- a/src/app/(journal)/[id].tsx +++ b/src/app/(journal)/[id].tsx @@ -3,7 +3,6 @@ import { useState } from "react"; import { Stack, useLocalSearchParams } from "expo-router"; import { EntryListView } from "@/components/entry/entry-list-view"; -import { JOURNALS } from "@/mocks/journals"; /** * ソート一覧 @@ -27,20 +26,18 @@ const SORT_LABELS: Record = { }; /** - * ジャーナル一覧 + * ジャーナル詳細(エントリー一覧) */ export default function JournalScreen() { - const { id } = useLocalSearchParams<{ id: string }>(); + const { id, name } = useLocalSearchParams<{ id: string; name: string }>(); const [searchText, setSearchText] = useState(""); const [sortKey, setSortKey] = useState("dateDesc"); - const journal = JOURNALS.find((journal) => journal.id === id); - return ( <> - + ); } diff --git a/src/app/(journal)/create.tsx b/src/app/(journal)/create.tsx index e32f797..23c0e8a 100644 --- a/src/app/(journal)/create.tsx +++ b/src/app/(journal)/create.tsx @@ -4,7 +4,7 @@ import { Stack, useRouter } from "expo-router"; import { SymbolView } from "expo-symbols"; import { JournalCreateView } from "@/components/journal/journal-create-view"; -import { useJournalField } from "@/hooks/journal/use-journal-field"; +import { useJournalField } from "@/utils/journal/use-journal-field"; /** * ジャーナル作成 @@ -24,9 +24,11 @@ export default function JournalCreateScreen() { formDisabled, } = useJournalField(); - const handleCreate = () => { - router.push(`/(journal)/1`); - createJournal(); + const handleCreate = async () => { + const { id, name } = await createJournal(); + + // replace でスタックせずにジャーナル詳細画面からジャーナル一覧へ戻れるようにする + router.replace(`/(journal)/${id}?name=${name}`); }; return ( @@ -35,7 +37,7 @@ export default function JournalCreateScreen() { options={{ title: "New Journal", headerRight: () => ( - + (); - const entry = ENTRIES.find((entry) => entry.id === id); + const { data: entry } = useLiveQuery(getEntryDetailQuery(id)); return ( <> [ { type: "menu", @@ -63,15 +64,7 @@ export default function EntryDetailScreen() { ], }} /> - {entry && ( - - )} + {entry && } ); } diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx index d82e11d..5d4f83c 100644 --- a/src/app/_layout.tsx +++ b/src/app/_layout.tsx @@ -1,7 +1,12 @@ import React from "react"; import AppTabs from "@/components/app-tabs"; +import { DrizzleProvider } from "@/components/drizzle-provider"; export default function RootLayout() { - return ; + return ( + + + + ); } diff --git a/src/components/drizzle-provider.tsx b/src/components/drizzle-provider.tsx new file mode 100644 index 0000000..f50ea84 --- /dev/null +++ b/src/components/drizzle-provider.tsx @@ -0,0 +1,33 @@ +import type { ReactNode } from "react"; + +import { useMigrations } from "drizzle-orm/expo-sqlite/migrator"; +import { useDrizzleStudio } from "expo-drizzle-studio-plugin"; + +import { db, expoDb } from "@/db/client"; + +import migrations from "../../drizzle/migrations"; + +type Props = { + /** + * React Node + */ + children: ReactNode; +}; + +/** + * ローカルDBプロバイダー + */ +export function DrizzleProvider({ children }: Props) { + const { success, error: migrateError } = useMigrations(db, migrations); + + useDrizzleStudio(expoDb); // Drizzle Studio の設定(開発環境でのみ有効) + + if (migrateError) { + console.error("Migration Error:", migrateError); + throw migrateError; + } + + if (!success) return null; + + return <>{children}; +} diff --git a/src/components/entry/entry-detail.tsx b/src/components/entry/entry-detail.tsx index 93228a9..4d002ce 100644 --- a/src/components/entry/entry-detail.tsx +++ b/src/components/entry/entry-detail.tsx @@ -1,21 +1,37 @@ import { PlatformColor, View } from "react-native"; -import { Host, List, Section, Text } from "@expo/ui/swift-ui"; -import { frame, listStyle } from "@expo/ui/swift-ui/modifiers"; +import { + Host, + HStack, + Image, + List, + Section, + Spacer, + Text, + VStack, +} from "@expo/ui/swift-ui"; +import { + font, + foregroundStyle, + frame, + listStyle, +} from "@expo/ui/swift-ui/modifiers"; -import { EntryObj } from "@/mocks/entries"; +import { EntryDetailObj } from "@/db/queries/entries"; import { formatDate } from "@/utils/date"; +type Props = { + /** エントリーデータ */ + entry: EntryDetailObj; +}; /** * エントリー詳細画面 */ -export function EntryDetailView({ - id, - date, - title, - preview, - bookmark, -}: EntryObj) { +export function EntryDetailView({ entry }: Props) { + const sorted = [...entry.values].sort( + (a, b) => a.field.sortOrder - b.field.sortOrder, + ); + return ( -
- {id} - {title} - {preview} - {bookmark ? "Bookmarked" : "Not bookmarked"} +
+ {formatDate(entry.createdAt)} + + {entry.bookmark && ( + + )} + + } + > + {sorted.map((v) => ( + + + {v.field.label} + + {v.value ?? ""} + + ))}
diff --git a/src/components/entry/entry-list-view.tsx b/src/components/entry/entry-list-view.tsx index 7f15eca..fe20435 100644 --- a/src/components/entry/entry-list-view.tsx +++ b/src/components/entry/entry-list-view.tsx @@ -6,13 +6,15 @@ import { headerProminence, moveDisabled, } from "@expo/ui/swift-ui/modifiers"; +import { useLiveQuery } from "drizzle-orm/expo-sqlite"; import { GlassView } from "expo-glass-effect"; import { useRouter } from "expo-router"; import { SymbolView } from "expo-symbols"; import type { SortKey } from "@/app/(journal)/[id]"; -import { ENTRIES, type EntryObj } from "@/mocks/entries"; +import { getEntriesQuery } from "@/db/queries/entries"; import { formatYearMonth } from "@/utils/date"; +import { EntryObj } from "@/utils/journal/use-entry"; import { EntryRow } from "./entry-row"; @@ -49,21 +51,42 @@ function groupByMonth( } type Props = { + /** ジャーナル id */ + id: string; + /** 検索テキスト */ searchText?: string; + /** ソートキー */ sortKey?: SortKey; }; +/** + * エントリー一覧画面 + */ export function EntryListView({ + id, searchText = "", sortKey = "dateDesc", }: Props) { const router = useRouter(); + const { data: dbEntries } = useLiveQuery(getEntriesQuery(id)); + + const entries: EntryObj[] = (dbEntries ?? []).map((entry) => ({ + id: entry.id, + date: new Date(entry.createdAt), + title: entry.values[0]?.value ?? "", + preview: entry.values + .slice(1) + .map((v) => v.value) + .join(" "), + bookmark: entry.bookmark, + })); + const filtered = searchText - ? ENTRIES.filter( + ? entries.filter( (e) => e.title.includes(searchText) || e.preview.includes(searchText), ) - : ENTRIES; + : entries; const sorted = sortEntries(filtered, sortKey); const grouped = groupByMonth(sorted); diff --git a/src/components/entry/entry-row.tsx b/src/components/entry/entry-row.tsx index a13df4a..34197fc 100644 --- a/src/components/entry/entry-row.tsx +++ b/src/components/entry/entry-row.tsx @@ -4,15 +4,20 @@ import { Button, HStack, Image, Spacer, Text, VStack } from "@expo/ui/swift-ui"; import { font, foregroundStyle, lineLimit } from "@expo/ui/swift-ui/modifiers"; import { useRouter } from "expo-router"; -import { EntryObj } from "@/mocks/entries"; import { formatDate } from "@/utils/date"; +import { EntryObj } from "@/utils/journal/use-entry"; const secondary = foregroundStyle({ type: "hierarchical", style: "secondary" }); +type Props = { + /** エントリーデータ */ + entry: EntryObj; +}; + /** * エントリー行 */ -export function EntryRow({ entry }: { entry: EntryObj }) { +export function EntryRow({ entry }: Props) { const router = useRouter(); return ( ); diff --git a/src/components/journal/field-bottom-sheet.tsx b/src/components/journal/field-bottom-sheet.tsx index ddb59d1..f316ca1 100644 --- a/src/components/journal/field-bottom-sheet.tsx +++ b/src/components/journal/field-bottom-sheet.tsx @@ -17,7 +17,7 @@ import { } from "@expo/ui/swift-ui/modifiers"; import { FIELD_ICONS, FIELD_LABELS, FieldType } from "@/core/constants"; -import { FIELD_TYPES } from "@/hooks/journal/use-journal-field"; +import { FIELD_TYPES } from "@/utils/journal/use-journal-field"; type Props = { /** ボトムシートの表示状態 */ diff --git a/src/components/journal/icon-select-bottom-sheet.tsx b/src/components/journal/icon-select-bottom-sheet.tsx index 6d8ddda..47453f4 100644 --- a/src/components/journal/icon-select-bottom-sheet.tsx +++ b/src/components/journal/icon-select-bottom-sheet.tsx @@ -18,7 +18,7 @@ import { presentationDetents, presentationDragIndicator, } from "@expo/ui/swift-ui/modifiers"; -import type { SFSymbol } from "sf-symbols-typescript"; +import { SFSymbol } from "expo-symbols"; import { JOURNAL_ICONS } from "@/core/constants"; diff --git a/src/components/journal/journal-card.tsx b/src/components/journal/journal-card.tsx index db2cbeb..e663082 100644 --- a/src/components/journal/journal-card.tsx +++ b/src/components/journal/journal-card.tsx @@ -8,29 +8,12 @@ import { padding, } from "@expo/ui/swift-ui/modifiers"; import { useRouter } from "expo-router"; -import type { SFSymbol } from "sf-symbols-typescript"; + +import { JournalWithCountObj } from "@/db/queries/journals"; type Props = { - /** - * ジャーナルID - */ - id: string; - /** - * ジャーナル名 - */ - name: string; - /** - * アイコン - */ - icon: SFSymbol; - /** - * カラー - */ - color: string; - /** - * エントリー数 - */ - count: number; + /** ジャーナルデータ */ + journal: JournalWithCountObj; }; function lightenColor(hex: string, amount = 40): string { @@ -44,8 +27,9 @@ function lightenColor(hex: string, amount = 40): string { /** * ジャーナルカード */ -export function JournalCard({ id, name, icon, color, count }: Props) { +export function JournalCard({ journal }: Props) { const router = useRouter(); + const { id, name, icon, color, entryCount } = journal; return ( router.push(`/(journal)/${id}`)), + onTapGesture(() => router.push(`/(journal)/${id}?name=${name}`)), ]} > - {count} + {entryCount} renameField(field.id, value)} modifiers={[frame({ maxWidth: 9999 })]} /> diff --git a/src/components/journal/journal-view.tsx b/src/components/journal/journal-view.tsx index 350c707..7e8227c 100644 --- a/src/components/journal/journal-view.tsx +++ b/src/components/journal/journal-view.tsx @@ -2,23 +2,22 @@ import { PlatformColor, View } from "react-native"; import { Grid, Host, ScrollView, VStack } from "@expo/ui/swift-ui"; import { padding } from "@expo/ui/swift-ui/modifiers"; +import { useLiveQuery } from "drizzle-orm/expo-sqlite"; import { JournalCard } from "@/components/journal/journal-card"; -import { JOURNALS } from "@/mocks/journals"; - -function chunkArray(arr: T[], size: number): T[][] { - const result: T[][] = []; - for (let i = 0; i < arr.length; i += size) { - result.push(arr.slice(i, i + size)); - } - return result; -} +import { getJournalsQuery } from "@/db/queries/journals"; +import { chunkArray } from "@/utils/chunk-array"; /** * ジャーナル一覧画面 */ export function JournalView() { - const rows = chunkArray(JOURNALS, 2); + const { data } = useLiveQuery(getJournalsQuery); + + const journals = data ?? []; + + // ジャーナルを2列にまとめる + const rows = chunkArray(journals, 2); return ( @@ -43,14 +42,7 @@ export function JournalView() { {rows.map((row, rowIndex) => ( {row.map((journal) => ( - + ))} ))} diff --git a/src/db/client.ts b/src/db/client.ts new file mode 100644 index 0000000..3037758 --- /dev/null +++ b/src/db/client.ts @@ -0,0 +1,11 @@ +import { drizzle } from "drizzle-orm/expo-sqlite"; +import { openDatabaseSync } from "expo-sqlite"; + +import * as schema from "@/db/schemas"; + +export const expoDb = openDatabaseSync("db.db", { enableChangeListener: true }); + +// 外部キー制約を有効化 +expoDb.execSync("PRAGMA foreign_keys = ON"); + +export const db = drizzle(expoDb, { schema }); diff --git a/src/db/queries/entries.ts b/src/db/queries/entries.ts new file mode 100644 index 0000000..3ed79d0 --- /dev/null +++ b/src/db/queries/entries.ts @@ -0,0 +1,30 @@ +import { db } from "@/db/client"; + +/** + * ジャーナルに紐付いたエントリー一覧を取得するクエリ + * @param journalId ジャーナルID + */ +export const getEntriesQuery = (journalId: string) => + db.query.entries.findMany({ + where: (entries, { eq }) => eq(entries.journalId, journalId), + with: { values: true }, + }); + +/** + * エントリー詳細を取得するクエリ + * @param entryId エントリーID + */ +export const getEntryDetailQuery = (entryId: string) => + db.query.entries.findFirst({ + where: (entries, { eq }) => eq(entries.id, entryId), + with: { + values: { + with: { field: true }, + }, + }, + }); + +/** エントリー詳細の型 */ +export type EntryDetailObj = NonNullable< + Awaited> +>; diff --git a/src/db/queries/journals.ts b/src/db/queries/journals.ts new file mode 100644 index 0000000..733bdf3 --- /dev/null +++ b/src/db/queries/journals.ts @@ -0,0 +1,41 @@ +import { sql } from "drizzle-orm"; + +import { db } from "@/db/client"; +import { entries, FieldlObj, fields, JournalObj, journals } from "@/db/schemas"; + +/** + * ジャーナル一覧を取得するクエリ + */ +export const getJournalsQuery = db.query.journals.findMany({ + extras: { + entryCount: + sql`(select count(*) from ${entries} where ${entries.journalId} = ${journals.id})`.as( + "entry_count", + ), + }, +}); + +/** + * ジャーナルをフィールドと共に作成するクエリを実行 + * @param journal ジャーナルのメタ情報 + * @param newFields フィールド一覧 + */ +export const storeJournal = async ( + journal: JournalObj, + newFields: FieldlObj[], +) => { + await db.transaction(async (tx) => { + await tx.insert(journals).values(journal); + + if (newFields.length > 0) { + await tx + .insert(fields) + .values( + newFields.map((field) => ({ ...field, journalId: journal.id })), + ); + } + }); +}; + +/** ジャーナル一覧の型 */ +export type JournalWithCountObj = Awaited[number]; diff --git a/src/db/schemas/entries.ts b/src/db/schemas/entries.ts new file mode 100644 index 0000000..198f776 --- /dev/null +++ b/src/db/schemas/entries.ts @@ -0,0 +1,68 @@ +import { relations } from "drizzle-orm"; +import { + integer, + sqliteTable, + text, + uniqueIndex, +} from "drizzle-orm/sqlite-core"; + +import { fields } from "./fields"; +import { journals } from "./journals"; + +/** + +/** + * エントリーテーブル + */ +export const entries = sqliteTable("entries", { + id: text().primaryKey(), + journalId: text() + .notNull() + .references(() => journals.id, { onDelete: "cascade" }), + bookmark: integer("bookmark", { mode: "boolean" }).notNull().default(false), + createdAt: integer() + .notNull() + .$defaultFn(() => Date.now()), + updatedAt: integer() + .notNull() + .$defaultFn(() => Date.now()), +}); + +/** + * エントリー値テーブル + * エントリーの各フィールドに対する入力値を管理する + * valueはフィールド種別に応じてJSON文字列で格納する + */ +export const entryValues = sqliteTable( + "entry_values", + { + id: text().primaryKey(), + entryId: text() + .notNull() + .references(() => entries.id, { onDelete: "cascade" }), + fieldId: text() + .notNull() + .references(() => fields.id, { onDelete: "cascade" }), + value: text(), + }, + (t) => [uniqueIndex("entry_field_unique").on(t.entryId, t.fieldId)], // ユニーク制約 +); + +export const entriesRelations = relations(entries, ({ one, many }) => ({ + journal: one(journals, { + fields: [entries.journalId], + references: [journals.id], + }), + values: many(entryValues), +})); + +export const entryValuesRelations = relations(entryValues, ({ one }) => ({ + entry: one(entries, { + fields: [entryValues.entryId], + references: [entries.id], + }), + field: one(fields, { + fields: [entryValues.fieldId], + references: [fields.id], + }), +})); diff --git a/src/db/schemas/fields.ts b/src/db/schemas/fields.ts new file mode 100644 index 0000000..77a5466 --- /dev/null +++ b/src/db/schemas/fields.ts @@ -0,0 +1,32 @@ +import { relations } from "drizzle-orm"; +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; + +import type { FieldType } from "@/core/constants"; + +import { entryValues } from "./entries"; +import { journals } from "./journals"; + +/** + * フィールド定義テーブル + */ +export const fields = sqliteTable("fields", { + id: text().primaryKey(), + journalId: text() + .notNull() + .references(() => journals.id, { onDelete: "cascade" }), + /** フィールドの種別 */ + type: text().notNull().$type(), + label: text().notNull(), + /** 表示順 */ + sortOrder: integer().notNull(), +}); + +export const fieldsRelations = relations(fields, ({ one, many }) => ({ + journal: one(journals, { + fields: [fields.journalId], + references: [journals.id], + }), + entryValues: many(entryValues), +})); + +export type FieldlObj = Omit; diff --git a/src/db/schemas/index.ts b/src/db/schemas/index.ts new file mode 100644 index 0000000..1090c31 --- /dev/null +++ b/src/db/schemas/index.ts @@ -0,0 +1,3 @@ +export * from "./journals"; +export * from "./fields"; +export * from "./entries"; diff --git a/src/db/schemas/journals.ts b/src/db/schemas/journals.ts new file mode 100644 index 0000000..242f5af --- /dev/null +++ b/src/db/schemas/journals.ts @@ -0,0 +1,31 @@ +import { relations } from "drizzle-orm"; +import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import type { SFSymbol } from "expo-symbols"; + +import { entries } from "./entries"; +import { fields } from "./fields"; + +/** + * ジャーナルテーブル + */ +export const journals = sqliteTable("journals", { + id: text().primaryKey(), + name: text().notNull(), + /** SFSymbol名 */ + icon: text().notNull().$type(), + /** HEXカラーコード */ + color: text().notNull(), + createdAt: integer() + .notNull() + .$defaultFn(() => Date.now()), + updatedAt: integer() + .notNull() + .$defaultFn(() => Date.now()), +}); + +export const journalsRelations = relations(journals, ({ many }) => ({ + fields: many(fields), + entries: many(entries), +})); + +export type JournalObj = typeof journals.$inferSelect; diff --git a/src/mocks/entries.ts b/src/mocks/entries.ts index c3377d6..449e207 100644 --- a/src/mocks/entries.ts +++ b/src/mocks/entries.ts @@ -1,10 +1,4 @@ -export type EntryObj = { - id: string; - date: Date; - title: string; - preview: string; - bookmark?: boolean; -}; +import { EntryObj } from "@/utils/journal/use-entry"; export const ENTRIES: EntryObj[] = [ { diff --git a/src/mocks/journals.ts b/src/mocks/journals.ts deleted file mode 100644 index 5fdd772..0000000 --- a/src/mocks/journals.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { SFSymbol } from "sf-symbols-typescript"; - -export type JournalObj = { - id: string; - name: string; - icon: SFSymbol; - color: string; - count: number; -}; - -export const JOURNALS: JournalObj[] = [ - { - id: "1", - name: "Morning Routine", - icon: "sun.max.fill", - color: "#FF9F0A", - count: 12, - }, - { id: "2", name: "Work Timer", icon: "timer", color: "#0A84FF", count: 5 }, - { - id: "3", - name: "Text Home", - icon: "message.fill", - color: "#30D158", - count: 8, - }, - { - id: "4", - name: "Play Music", - icon: "music.note", - color: "#BF5AF2", - count: 3, - }, - { - id: "5", - name: "Set Alarm", - icon: "alarm.fill", - color: "#FF375F", - count: 20, - }, - { - id: "6", - name: "Location", - icon: "location.fill", - color: "#5E5CE6", - count: 1, - }, - { - id: "7", - name: "Log Water", - icon: "drop.fill", - color: "#32ADE6", - count: 15, - }, - { - id: "8", - name: "Focus Mode", - icon: "moon.fill", - color: "#636366", - count: 7, - }, -]; diff --git a/src/utils/chunk-array.ts b/src/utils/chunk-array.ts new file mode 100644 index 0000000..6ddb40c --- /dev/null +++ b/src/utils/chunk-array.ts @@ -0,0 +1,12 @@ +/** + * 配列を指定したサイズのチャンクに分割する + * @param arr 分割する配列 + * @param size チャンクのサイズ + */ +export const chunkArray = (arr: T[], size: number): T[][] => { + const result: T[][] = []; + for (let i = 0; i < arr.length; i += size) { + result.push(arr.slice(i, i + size)); + } + return result; +}; diff --git a/src/utils/date.ts b/src/utils/date.ts index 2d3df44..c23ff59 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -3,7 +3,7 @@ * @param date フォーマットする日付 * @return フォーマットされた日付文字列 */ -export const formatDate = (date: Date): string => { +export const formatDate = (date: Date | number): string => { return new Intl.DateTimeFormat(undefined, { year: "numeric", month: "long", @@ -17,7 +17,7 @@ export const formatDate = (date: Date): string => { * @param date フォーマットする日付 * @returns フォーマットされた年月文字列 */ -export const formatYearMonth = (date: Date): string => { +export const formatYearMonth = (date: Date | number): string => { return new Intl.DateTimeFormat(undefined, { year: "numeric", month: "long", diff --git a/src/utils/journal/use-entry.ts b/src/utils/journal/use-entry.ts new file mode 100644 index 0000000..d6bd3dc --- /dev/null +++ b/src/utils/journal/use-entry.ts @@ -0,0 +1,7 @@ +export type EntryObj = { + id: string; + date: Date; + title: string; + preview: string; + bookmark?: boolean; +}; diff --git a/src/hooks/journal/use-journal-field.ts b/src/utils/journal/use-journal-field.ts similarity index 75% rename from src/hooks/journal/use-journal-field.ts rename to src/utils/journal/use-journal-field.ts index ed8c566..a9eab28 100644 --- a/src/hooks/journal/use-journal-field.ts +++ b/src/utils/journal/use-journal-field.ts @@ -1,18 +1,11 @@ import { useState } from "react"; import * as Crypto from "expo-crypto"; -import type { SFSymbol } from "sf-symbols-typescript"; +import { SFSymbol } from "expo-symbols"; import { FIELD_ICONS, FieldType, JOURNAL_ICONS } from "@/core/constants"; - -/** - * ジャーナルの型 - */ -export type JournalObj = { - id: string; - meta: JournalMetaObj; - fields: FieldObj[]; -}; +import { storeJournal } from "@/db/queries/journals"; +import { FieldlObj, JournalObj } from "@/db/schemas"; /** * ジャーナルメタ情報の型 @@ -50,7 +43,7 @@ export const FIELD_TYPES = Object.keys(FIELD_ICONS) as FieldType[]; * - moveField フィールドを並び替えする関数 * - meta ジャーナルのメタ情報 * - setMeta ジャーナルのメタ情報をセットする関数 - * - createJournal ジャーナルを作成する関数 + * - createJournal 新規ジャーナルを作成する関数 * - formDisabled フォームが送信可能かどうかのフラグ */ export const useJournalField = () => { @@ -68,7 +61,7 @@ export const useJournalField = () => { * 新規フィールドを追加する * @param type 追加するフィールドの種別 */ - const addField = (type: FieldType) => { + const addField = (type: FieldType): void => { const newField: FieldObj = { id: Crypto.randomUUID(), type, @@ -82,7 +75,7 @@ export const useJournalField = () => { * @param id ラベルを編集する id * @param newLabel 新しいラベル */ - const renameField = (id: string, newLabel: string) => { + const renameField = (id: string, newLabel: string): void => { setFields((prev) => prev.map((field) => field.id === id ? { ...field, label: newLabel } : field, @@ -94,7 +87,7 @@ export const useJournalField = () => { * インデックス指定でフィールドを削除する(List.ForEach の onDelete 用) * @param indices 削除するインデックスの配列 */ - const deleteField = (indices: number[]) => { + const deleteField = (indices: number[]): void => { setFields((prev) => prev.filter((_, i) => !indices.includes(i))); }; @@ -103,7 +96,7 @@ export const useJournalField = () => { * @param sourceIndices 移動元のインデックス配列 * @param destination 移動先のインデックス */ - const moveField = (sourceIndices: number[], destination: number) => { + const moveField = (sourceIndices: number[], destination: number): void => { setFields((prev) => { const next = [...prev]; const moved = sourceIndices.map((i) => next[i]); @@ -122,14 +115,30 @@ export const useJournalField = () => { fields.some((field) => field.label.length === 0); /** - * ジャーナルを作成する関数 + * 新規ジャーナルをフィールドと共にDBに保存する */ - const createJournal = () => { - if (!formDisabled) { - const newJournal: JournalObj = { id: Crypto.randomUUID(), meta, fields }; - console.log(newJournal); - return newJournal; - } + const createJournal = async (): Promise => { + const now = Date.now(); + + const newJournal: JournalObj = { + id: Crypto.randomUUID(), + name: meta.name, + icon: meta.icon, + color: meta.color, + createdAt: now, + updatedAt: now, + }; + + const newFieldsFieldlObj: FieldlObj[] = fields.map((field, i) => ({ + id: field.id, + type: field.type, + label: field.label, + sortOrder: i, + })); + + await storeJournal(newJournal, newFieldsFieldlObj); + + return newJournal; }; return {