@hasna/contacts 0.6.33 → 0.6.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/dist/cli/index.js +45 -11
  2. package/dist/index.js +3 -0
  3. package/dist/server/cloud-index.js +45 -11
  4. package/dist/server/index.js +45 -11
  5. package/migrations/0013_remote_delete_tombstones.sql +15 -0
  6. package/migrations/0014_contact_tag_filter_index.sql +9 -0
  7. package/package.json +1 -1
  8. package/dashboard/dist/assets/index-0l6aQb1t.css +0 -1
  9. package/dashboard/dist/assets/index-opnZdkVD.js +0 -229
  10. package/dashboard/dist/index.html +0 -13
  11. package/dist/cli/commands/advanced.d.ts +0 -3
  12. package/dist/cli/commands/advanced.d.ts.map +0 -1
  13. package/dist/cli/commands/audience.d.ts +0 -18
  14. package/dist/cli/commands/audience.d.ts.map +0 -1
  15. package/dist/cli/commands/core.d.ts +0 -3
  16. package/dist/cli/commands/core.d.ts.map +0 -1
  17. package/dist/cli/commands/crm.d.ts +0 -3
  18. package/dist/cli/commands/crm.d.ts.map +0 -1
  19. package/dist/cli/index.d.ts +0 -3
  20. package/dist/cli/index.d.ts.map +0 -1
  21. package/dist/cli/storage.d.ts +0 -3
  22. package/dist/cli/storage.d.ts.map +0 -1
  23. package/dist/cli/utils.d.ts +0 -10
  24. package/dist/cli/utils.d.ts.map +0 -1
  25. package/dist/cloud/http-storage.d.ts +0 -88
  26. package/dist/cloud/http-storage.d.ts.map +0 -1
  27. package/dist/db/activity.d.ts +0 -15
  28. package/dist/db/activity.d.ts.map +0 -1
  29. package/dist/db/applications.d.ts +0 -10
  30. package/dist/db/applications.d.ts.map +0 -1
  31. package/dist/db/audiences.d.ts +0 -51
  32. package/dist/db/audiences.d.ts.map +0 -1
  33. package/dist/db/companies.d.ts +0 -19
  34. package/dist/db/companies.d.ts.map +0 -1
  35. package/dist/db/contact-tasks.d.ts +0 -19
  36. package/dist/db/contact-tasks.d.ts.map +0 -1
  37. package/dist/db/contacts.d.ts +0 -30
  38. package/dist/db/contacts.d.ts.map +0 -1
  39. package/dist/db/coordination.d.ts +0 -30
  40. package/dist/db/coordination.d.ts.map +0 -1
  41. package/dist/db/database.d.ts +0 -8
  42. package/dist/db/database.d.ts.map +0 -1
  43. package/dist/db/deals.d.ts +0 -14
  44. package/dist/db/deals.d.ts.map +0 -1
  45. package/dist/db/documents.d.ts +0 -38
  46. package/dist/db/documents.d.ts.map +0 -1
  47. package/dist/db/events.d.ts +0 -14
  48. package/dist/db/events.d.ts.map +0 -1
  49. package/dist/db/field-history.d.ts +0 -17
  50. package/dist/db/field-history.d.ts.map +0 -1
  51. package/dist/db/freshness.d.ts +0 -24
  52. package/dist/db/freshness.d.ts.map +0 -1
  53. package/dist/db/graph.d.ts +0 -21
  54. package/dist/db/graph.d.ts.map +0 -1
  55. package/dist/db/groups.d.ts +0 -22
  56. package/dist/db/groups.d.ts.map +0 -1
  57. package/dist/db/health.d.ts +0 -40
  58. package/dist/db/health.d.ts.map +0 -1
  59. package/dist/db/identity.d.ts +0 -32
  60. package/dist/db/identity.d.ts.map +0 -1
  61. package/dist/db/job-history.d.ts +0 -29
  62. package/dist/db/job-history.d.ts.map +0 -1
  63. package/dist/db/learnings.d.ts +0 -43
  64. package/dist/db/learnings.d.ts.map +0 -1
  65. package/dist/db/notes.d.ts +0 -8
  66. package/dist/db/notes.d.ts.map +0 -1
  67. package/dist/db/org-chart.d.ts +0 -37
  68. package/dist/db/org-chart.d.ts.map +0 -1
  69. package/dist/db/org-members.d.ts +0 -9
  70. package/dist/db/org-members.d.ts.map +0 -1
  71. package/dist/db/paths.d.ts +0 -3
  72. package/dist/db/paths.d.ts.map +0 -1
  73. package/dist/db/pg-migrations.d.ts +0 -8
  74. package/dist/db/pg-migrations.d.ts.map +0 -1
  75. package/dist/db/relationships.d.ts +0 -30
  76. package/dist/db/relationships.d.ts.map +0 -1
  77. package/dist/db/signals.d.ts +0 -18
  78. package/dist/db/signals.d.ts.map +0 -1
  79. package/dist/db/sqlite-adapter.d.ts +0 -15
  80. package/dist/db/sqlite-adapter.d.ts.map +0 -1
  81. package/dist/db/storage.d.ts +0 -15
  82. package/dist/db/storage.d.ts.map +0 -1
  83. package/dist/db/tags.d.ts +0 -14
  84. package/dist/db/tags.d.ts.map +0 -1
  85. package/dist/db/tombstones.d.ts +0 -19
  86. package/dist/db/tombstones.d.ts.map +0 -1
  87. package/dist/db/vendor-comms.d.ts +0 -15
  88. package/dist/db/vendor-comms.d.ts.map +0 -1
  89. package/dist/generated/storage-kit/health.d.ts +0 -20
  90. package/dist/generated/storage-kit/health.d.ts.map +0 -1
  91. package/dist/generated/storage-kit/index.d.ts +0 -8
  92. package/dist/generated/storage-kit/index.d.ts.map +0 -1
  93. package/dist/generated/storage-kit/migrations.d.ts +0 -48
  94. package/dist/generated/storage-kit/migrations.d.ts.map +0 -1
  95. package/dist/generated/storage-kit/mode.d.ts +0 -48
  96. package/dist/generated/storage-kit/mode.d.ts.map +0 -1
  97. package/dist/generated/storage-kit/pool.d.ts +0 -34
  98. package/dist/generated/storage-kit/pool.d.ts.map +0 -1
  99. package/dist/generated/storage-kit/query.d.ts +0 -36
  100. package/dist/generated/storage-kit/query.d.ts.map +0 -1
  101. package/dist/generated/storage-kit/tls.d.ts +0 -26
  102. package/dist/generated/storage-kit/tls.d.ts.map +0 -1
  103. package/dist/index.d.ts +0 -7
  104. package/dist/index.d.ts.map +0 -1
  105. package/dist/lib/apple-contacts.d.ts +0 -3
  106. package/dist/lib/apple-contacts.d.ts.map +0 -1
  107. package/dist/lib/audience-contract.d.ts +0 -215
  108. package/dist/lib/audience-contract.d.ts.map +0 -1
  109. package/dist/lib/audit.d.ts +0 -12
  110. package/dist/lib/audit.d.ts.map +0 -1
  111. package/dist/lib/brief.d.ts +0 -3
  112. package/dist/lib/brief.d.ts.map +0 -1
  113. package/dist/lib/config.d.ts +0 -7
  114. package/dist/lib/config.d.ts.map +0 -1
  115. package/dist/lib/connector.d.ts +0 -44
  116. package/dist/lib/connector.d.ts.map +0 -1
  117. package/dist/lib/context.d.ts +0 -5
  118. package/dist/lib/context.d.ts.map +0 -1
  119. package/dist/lib/dedup.d.ts +0 -10
  120. package/dist/lib/dedup.d.ts.map +0 -1
  121. package/dist/lib/document-scanner.d.ts +0 -8
  122. package/dist/lib/document-scanner.d.ts.map +0 -1
  123. package/dist/lib/embeddings.d.ts +0 -9
  124. package/dist/lib/embeddings.d.ts.map +0 -1
  125. package/dist/lib/export.d.ts +0 -8
  126. package/dist/lib/export.d.ts.map +0 -1
  127. package/dist/lib/freshness.d.ts +0 -19
  128. package/dist/lib/freshness.d.ts.map +0 -1
  129. package/dist/lib/gmail-import.d.ts +0 -51
  130. package/dist/lib/gmail-import.d.ts.map +0 -1
  131. package/dist/lib/google-contacts.d.ts +0 -115
  132. package/dist/lib/google-contacts.d.ts.map +0 -1
  133. package/dist/lib/images.d.ts +0 -30
  134. package/dist/lib/images.d.ts.map +0 -1
  135. package/dist/lib/import.d.ts +0 -10
  136. package/dist/lib/import.d.ts.map +0 -1
  137. package/dist/lib/learning-maintenance.d.ts +0 -8
  138. package/dist/lib/learning-maintenance.d.ts.map +0 -1
  139. package/dist/lib/mailery-sync.d.ts +0 -46
  140. package/dist/lib/mailery-sync.d.ts.map +0 -1
  141. package/dist/lib/meeting-capture.d.ts +0 -15
  142. package/dist/lib/meeting-capture.d.ts.map +0 -1
  143. package/dist/lib/package-version.d.ts +0 -6
  144. package/dist/lib/package-version.d.ts.map +0 -1
  145. package/dist/lib/signals.d.ts +0 -9
  146. package/dist/lib/signals.d.ts.map +0 -1
  147. package/dist/lib/signature-parser.d.ts +0 -36
  148. package/dist/lib/signature-parser.d.ts.map +0 -1
  149. package/dist/lib/stats.d.ts +0 -24
  150. package/dist/lib/stats.d.ts.map +0 -1
  151. package/dist/lib/timeline.d.ts +0 -11
  152. package/dist/lib/timeline.d.ts.map +0 -1
  153. package/dist/lib/upcoming.d.ts +0 -14
  154. package/dist/lib/upcoming.d.ts.map +0 -1
  155. package/dist/lib/vault.d.ts +0 -24
  156. package/dist/lib/vault.d.ts.map +0 -1
  157. package/dist/mcp/handlers/advanced.d.ts +0 -14
  158. package/dist/mcp/handlers/advanced.d.ts.map +0 -1
  159. package/dist/mcp/handlers/audience.d.ts +0 -9
  160. package/dist/mcp/handlers/audience.d.ts.map +0 -1
  161. package/dist/mcp/handlers/core.d.ts +0 -10
  162. package/dist/mcp/handlers/core.d.ts.map +0 -1
  163. package/dist/mcp/handlers/crm.d.ts +0 -14
  164. package/dist/mcp/handlers/crm.d.ts.map +0 -1
  165. package/dist/mcp/handlers/index.d.ts +0 -4
  166. package/dist/mcp/handlers/index.d.ts.map +0 -1
  167. package/dist/mcp/handlers/types.d.ts +0 -10
  168. package/dist/mcp/handlers/types.d.ts.map +0 -1
  169. package/dist/mcp/http.d.ts +0 -17
  170. package/dist/mcp/http.d.ts.map +0 -1
  171. package/dist/mcp/index.d.ts +0 -5
  172. package/dist/mcp/index.d.ts.map +0 -1
  173. package/dist/mcp/register-tools.d.ts +0 -3
  174. package/dist/mcp/register-tools.d.ts.map +0 -1
  175. package/dist/mcp/schema.d.ts +0 -3
  176. package/dist/mcp/schema.d.ts.map +0 -1
  177. package/dist/mcp/storage-tools.d.ts +0 -3
  178. package/dist/mcp/storage-tools.d.ts.map +0 -1
  179. package/dist/mcp/tools.d.ts +0 -3799
  180. package/dist/mcp/tools.d.ts.map +0 -1
  181. package/dist/sdk/index.d.ts +0 -16
  182. package/dist/sdk/index.d.ts.map +0 -1
  183. package/dist/sdk/v1.generated.d.ts +0 -203
  184. package/dist/sdk/v1.generated.d.ts.map +0 -1
  185. package/dist/server/cloud-index.d.ts +0 -3
  186. package/dist/server/cloud-index.d.ts.map +0 -1
  187. package/dist/server/cloud-serve.d.ts +0 -3
  188. package/dist/server/cloud-serve.d.ts.map +0 -1
  189. package/dist/server/cloud.d.ts +0 -53
  190. package/dist/server/cloud.d.ts.map +0 -1
  191. package/dist/server/index.d.ts +0 -3
  192. package/dist/server/index.d.ts.map +0 -1
  193. package/dist/server/openapi.d.ts +0 -765
  194. package/dist/server/openapi.d.ts.map +0 -1
  195. package/dist/server/pg-store.d.ts +0 -861
  196. package/dist/server/pg-store.d.ts.map +0 -1
  197. package/dist/server/security.d.ts +0 -24
  198. package/dist/server/security.d.ts.map +0 -1
  199. package/dist/server/serve.d.ts +0 -9
  200. package/dist/server/serve.d.ts.map +0 -1
  201. package/dist/server/v1.d.ts +0 -6
  202. package/dist/server/v1.d.ts.map +0 -1
  203. package/dist/store/index.d.ts +0 -383
  204. package/dist/store/index.d.ts.map +0 -1
  205. package/dist/types/index.d.ts +0 -856
  206. package/dist/types/index.d.ts.map +0 -1
package/dist/cli/index.js CHANGED
@@ -33708,6 +33708,14 @@ var init_pg_migrations = __esm(() => {
33708
33708
  CREATE INDEX IF NOT EXISTS idx_contacts_tombstones_deleted_at ON _contacts_tombstones(deleted_at);
33709
33709
 
33710
33710
  INSERT INTO _migrations (version) VALUES (12) ON CONFLICT DO NOTHING;
33711
+ `,
33712
+ `
33713
+ -- The contact_tags primary key is (contact_id, tag_id), which does not
33714
+ -- support the public tag_id filter efficiently. Keep this forward-only and
33715
+ -- idempotent for populated cloud deployments.
33716
+ CREATE INDEX IF NOT EXISTS idx_contact_tags_tag_contact ON contact_tags(tag_id, contact_id);
33717
+
33718
+ INSERT INTO _migrations (version) VALUES (13) ON CONFLICT DO NOTHING;
33711
33719
  `
33712
33720
  ];
33713
33721
  });
@@ -33917,6 +33925,19 @@ class ContactsPgStore {
33917
33925
  constructor(client) {
33918
33926
  this.client = client;
33919
33927
  }
33928
+ async attachTags(contacts) {
33929
+ const tagsByContactId = new Map(contacts.map((contact) => [contact.id, []]));
33930
+ if (contacts.length === 0)
33931
+ return [];
33932
+ const rows = await this.client.many(`SELECT ct.contact_id, t.*
33933
+ FROM contact_tags ct
33934
+ JOIN tags t ON t.id = ct.tag_id
33935
+ WHERE ct.contact_id = ANY($1::text[])
33936
+ ORDER BY t.name ASC`, [contacts.map((contact) => contact.id)]);
33937
+ for (const row of rows)
33938
+ tagsByContactId.get(row.contact_id)?.push(mapTag(row));
33939
+ return contacts.map((contact) => ({ ...contact, tags: tagsByContactId.get(contact.id) ?? [] }));
33940
+ }
33920
33941
  async listContacts(filter = {}) {
33921
33942
  const limit = Math.min(Math.max(filter.limit ?? 50, 1), 500);
33922
33943
  const offset = Math.max(filter.offset ?? 0, 0);
@@ -33930,6 +33951,10 @@ class ContactsPgStore {
33930
33951
  params.push(filter.status);
33931
33952
  where.push(`status = $${params.length}`);
33932
33953
  }
33954
+ if (filter.tag_id) {
33955
+ params.push(filter.tag_id);
33956
+ where.push(`EXISTS (SELECT 1 FROM contact_tags ct WHERE ct.contact_id = contacts.id AND ct.tag_id = $${params.length})`);
33957
+ }
33933
33958
  if (filter.q) {
33934
33959
  params.push(filter.q);
33935
33960
  where.push(`search_vector @@ plainto_tsquery('simple', $${params.length})`);
@@ -33938,11 +33963,13 @@ class ContactsPgStore {
33938
33963
  const countRow = await this.client.get(`SELECT COUNT(*)::text AS count FROM contacts ${whereSql}`, params);
33939
33964
  params.push(limit, offset);
33940
33965
  const rows = await this.client.many(`SELECT * FROM contacts ${whereSql} ORDER BY display_name ASC LIMIT $${params.length - 1} OFFSET $${params.length}`, params);
33941
- return { contacts: rows.map(mapContact), count: Number(countRow?.count ?? rows.length) };
33966
+ return { contacts: await this.attachTags(rows.map(mapContact)), count: Number(countRow?.count ?? rows.length) };
33942
33967
  }
33943
33968
  async getContact(id) {
33944
33969
  const row = await this.client.get(`SELECT * FROM contacts WHERE id = $1`, [id]);
33945
- return row ? mapContact(row) : null;
33970
+ if (!row)
33971
+ return null;
33972
+ return (await this.attachTags([mapContact(row)]))[0];
33946
33973
  }
33947
33974
  async createContact(input) {
33948
33975
  const id = uuid3();
@@ -33978,7 +34005,7 @@ class ContactsPgStore {
33978
34005
  input.priority ?? 3,
33979
34006
  input.timezone ?? null
33980
34007
  ]);
33981
- return mapContact(row);
34008
+ return (await this.attachTags([mapContact(row)]))[0];
33982
34009
  }
33983
34010
  async updateContact(id, input) {
33984
34011
  const allowed = {};
@@ -34018,7 +34045,7 @@ class ContactsPgStore {
34018
34045
  const sets = keys.map((k, i) => `${k} = $${i + 2}`);
34019
34046
  sets.push(`updated_at = NOW()`);
34020
34047
  const row = await this.client.get(`UPDATE contacts SET ${sets.join(", ")} WHERE id = $1 RETURNING *`, [id, ...keys.map((k) => allowed[k])]);
34021
- return row ? mapContact(row) : null;
34048
+ return row ? (await this.attachTags([mapContact(row)]))[0] : null;
34022
34049
  }
34023
34050
  async deleteContact(id) {
34024
34051
  const result = await this.client.query(`DELETE FROM contacts WHERE id = $1`, [id]);
@@ -35479,6 +35506,16 @@ async function readJson(req) {
35479
35506
  return null;
35480
35507
  }
35481
35508
  }
35509
+ function contactListFilterFromUrl(url) {
35510
+ return {
35511
+ ...url.searchParams.get("company_id") ? { company_id: url.searchParams.get("company_id") } : {},
35512
+ ...url.searchParams.get("status") ? { status: url.searchParams.get("status") } : {},
35513
+ ...url.searchParams.get("tag_id") ? { tag_id: url.searchParams.get("tag_id") } : {},
35514
+ ...url.searchParams.get("q") ? { q: url.searchParams.get("q") } : {},
35515
+ ...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
35516
+ ...url.searchParams.get("offset") ? { offset: Number(url.searchParams.get("offset")) } : {}
35517
+ };
35518
+ }
35482
35519
  async function handleV1Request(req, url) {
35483
35520
  const path = url.pathname;
35484
35521
  if (path !== "/v1" && !path.startsWith("/v1/"))
@@ -35592,13 +35629,7 @@ async function handleV1Request(req, url) {
35592
35629
  if (resource === "contacts") {
35593
35630
  if (!id) {
35594
35631
  if (method === "GET") {
35595
- const result = await store.listContacts({
35596
- ...url.searchParams.get("company_id") ? { company_id: url.searchParams.get("company_id") } : {},
35597
- ...url.searchParams.get("status") ? { status: url.searchParams.get("status") } : {},
35598
- ...url.searchParams.get("q") ? { q: url.searchParams.get("q") } : {},
35599
- ...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
35600
- ...url.searchParams.get("offset") ? { offset: Number(url.searchParams.get("offset")) } : {}
35601
- });
35632
+ const result = await store.listContacts(contactListFilterFromUrl(url));
35602
35633
  return json5(result);
35603
35634
  }
35604
35635
  if (method === "POST") {
@@ -36365,6 +36396,7 @@ function buildV1OpenApiDocument(version2 = getPackageVersion()) {
36365
36396
  { name: "q", in: "query", schema: { type: "string" } },
36366
36397
  { name: "company_id", in: "query", schema: { type: "string" } },
36367
36398
  { name: "status", in: "query", schema: { type: "string" } },
36399
+ { name: "tag_id", in: "query", schema: { type: "string" } },
36368
36400
  { name: "limit", in: "query", schema: { type: "number" } },
36369
36401
  { name: "offset", in: "query", schema: { type: "number" } }
36370
36402
  ],
@@ -36570,6 +36602,7 @@ var init_openapi = __esm(() => {
36570
36602
  init_package_version();
36571
36603
  contactSchema = {
36572
36604
  type: "object",
36605
+ required: ["tags"],
36573
36606
  properties: {
36574
36607
  id: { type: "string" },
36575
36608
  first_name: { type: "string" },
@@ -36584,6 +36617,7 @@ var init_openapi = __esm(() => {
36584
36617
  sensitivity: { type: "string" },
36585
36618
  archived: { type: "boolean" },
36586
36619
  priority: { type: "number" },
36620
+ tags: { type: "array", items: { $ref: "#/components/schemas/Tag" } },
36587
36621
  created_at: { type: "string" },
36588
36622
  updated_at: { type: "string" }
36589
36623
  }
package/dist/index.js CHANGED
@@ -6123,6 +6123,7 @@ function getPackageVersion() {
6123
6123
  // src/server/openapi.ts
6124
6124
  var contactSchema = {
6125
6125
  type: "object",
6126
+ required: ["tags"],
6126
6127
  properties: {
6127
6128
  id: { type: "string" },
6128
6129
  first_name: { type: "string" },
@@ -6137,6 +6138,7 @@ var contactSchema = {
6137
6138
  sensitivity: { type: "string" },
6138
6139
  archived: { type: "boolean" },
6139
6140
  priority: { type: "number" },
6141
+ tags: { type: "array", items: { $ref: "#/components/schemas/Tag" } },
6140
6142
  created_at: { type: "string" },
6141
6143
  updated_at: { type: "string" }
6142
6144
  }
@@ -6269,6 +6271,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
6269
6271
  { name: "q", in: "query", schema: { type: "string" } },
6270
6272
  { name: "company_id", in: "query", schema: { type: "string" } },
6271
6273
  { name: "status", in: "query", schema: { type: "string" } },
6274
+ { name: "tag_id", in: "query", schema: { type: "string" } },
6272
6275
  { name: "limit", in: "query", schema: { type: "number" } },
6273
6276
  { name: "offset", in: "query", schema: { type: "number" } }
6274
6277
  ],
@@ -6170,6 +6170,14 @@ var PG_MIGRATIONS = [
6170
6170
  CREATE INDEX IF NOT EXISTS idx_contacts_tombstones_deleted_at ON _contacts_tombstones(deleted_at);
6171
6171
 
6172
6172
  INSERT INTO _migrations (version) VALUES (12) ON CONFLICT DO NOTHING;
6173
+ `,
6174
+ `
6175
+ -- The contact_tags primary key is (contact_id, tag_id), which does not
6176
+ -- support the public tag_id filter efficiently. Keep this forward-only and
6177
+ -- idempotent for populated cloud deployments.
6178
+ CREATE INDEX IF NOT EXISTS idx_contact_tags_tag_contact ON contact_tags(tag_id, contact_id);
6179
+
6180
+ INSERT INTO _migrations (version) VALUES (13) ON CONFLICT DO NOTHING;
6173
6181
  `
6174
6182
  ];
6175
6183
 
@@ -6281,6 +6289,7 @@ async function closeCloud() {
6281
6289
  // src/server/openapi.ts
6282
6290
  var contactSchema = {
6283
6291
  type: "object",
6292
+ required: ["tags"],
6284
6293
  properties: {
6285
6294
  id: { type: "string" },
6286
6295
  first_name: { type: "string" },
@@ -6295,6 +6304,7 @@ var contactSchema = {
6295
6304
  sensitivity: { type: "string" },
6296
6305
  archived: { type: "boolean" },
6297
6306
  priority: { type: "number" },
6307
+ tags: { type: "array", items: { $ref: "#/components/schemas/Tag" } },
6298
6308
  created_at: { type: "string" },
6299
6309
  updated_at: { type: "string" }
6300
6310
  }
@@ -6427,6 +6437,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
6427
6437
  { name: "q", in: "query", schema: { type: "string" } },
6428
6438
  { name: "company_id", in: "query", schema: { type: "string" } },
6429
6439
  { name: "status", in: "query", schema: { type: "string" } },
6440
+ { name: "tag_id", in: "query", schema: { type: "string" } },
6430
6441
  { name: "limit", in: "query", schema: { type: "number" } },
6431
6442
  { name: "offset", in: "query", schema: { type: "number" } }
6432
6443
  ],
@@ -6736,6 +6747,19 @@ class ContactsPgStore {
6736
6747
  constructor(client) {
6737
6748
  this.client = client;
6738
6749
  }
6750
+ async attachTags(contacts) {
6751
+ const tagsByContactId = new Map(contacts.map((contact) => [contact.id, []]));
6752
+ if (contacts.length === 0)
6753
+ return [];
6754
+ const rows = await this.client.many(`SELECT ct.contact_id, t.*
6755
+ FROM contact_tags ct
6756
+ JOIN tags t ON t.id = ct.tag_id
6757
+ WHERE ct.contact_id = ANY($1::text[])
6758
+ ORDER BY t.name ASC`, [contacts.map((contact) => contact.id)]);
6759
+ for (const row of rows)
6760
+ tagsByContactId.get(row.contact_id)?.push(mapTag(row));
6761
+ return contacts.map((contact) => ({ ...contact, tags: tagsByContactId.get(contact.id) ?? [] }));
6762
+ }
6739
6763
  async listContacts(filter = {}) {
6740
6764
  const limit = Math.min(Math.max(filter.limit ?? 50, 1), 500);
6741
6765
  const offset = Math.max(filter.offset ?? 0, 0);
@@ -6749,6 +6773,10 @@ class ContactsPgStore {
6749
6773
  params.push(filter.status);
6750
6774
  where.push(`status = $${params.length}`);
6751
6775
  }
6776
+ if (filter.tag_id) {
6777
+ params.push(filter.tag_id);
6778
+ where.push(`EXISTS (SELECT 1 FROM contact_tags ct WHERE ct.contact_id = contacts.id AND ct.tag_id = $${params.length})`);
6779
+ }
6752
6780
  if (filter.q) {
6753
6781
  params.push(filter.q);
6754
6782
  where.push(`search_vector @@ plainto_tsquery('simple', $${params.length})`);
@@ -6757,11 +6785,13 @@ class ContactsPgStore {
6757
6785
  const countRow = await this.client.get(`SELECT COUNT(*)::text AS count FROM contacts ${whereSql}`, params);
6758
6786
  params.push(limit, offset);
6759
6787
  const rows = await this.client.many(`SELECT * FROM contacts ${whereSql} ORDER BY display_name ASC LIMIT $${params.length - 1} OFFSET $${params.length}`, params);
6760
- return { contacts: rows.map(mapContact), count: Number(countRow?.count ?? rows.length) };
6788
+ return { contacts: await this.attachTags(rows.map(mapContact)), count: Number(countRow?.count ?? rows.length) };
6761
6789
  }
6762
6790
  async getContact(id) {
6763
6791
  const row = await this.client.get(`SELECT * FROM contacts WHERE id = $1`, [id]);
6764
- return row ? mapContact(row) : null;
6792
+ if (!row)
6793
+ return null;
6794
+ return (await this.attachTags([mapContact(row)]))[0];
6765
6795
  }
6766
6796
  async createContact(input) {
6767
6797
  const id = uuid();
@@ -6797,7 +6827,7 @@ class ContactsPgStore {
6797
6827
  input.priority ?? 3,
6798
6828
  input.timezone ?? null
6799
6829
  ]);
6800
- return mapContact(row);
6830
+ return (await this.attachTags([mapContact(row)]))[0];
6801
6831
  }
6802
6832
  async updateContact(id, input) {
6803
6833
  const allowed = {};
@@ -6837,7 +6867,7 @@ class ContactsPgStore {
6837
6867
  const sets = keys.map((k, i) => `${k} = $${i + 2}`);
6838
6868
  sets.push(`updated_at = NOW()`);
6839
6869
  const row = await this.client.get(`UPDATE contacts SET ${sets.join(", ")} WHERE id = $1 RETURNING *`, [id, ...keys.map((k) => allowed[k])]);
6840
- return row ? mapContact(row) : null;
6870
+ return row ? (await this.attachTags([mapContact(row)]))[0] : null;
6841
6871
  }
6842
6872
  async deleteContact(id) {
6843
6873
  const result = await this.client.query(`DELETE FROM contacts WHERE id = $1`, [id]);
@@ -8299,6 +8329,16 @@ async function readJson(req) {
8299
8329
  return null;
8300
8330
  }
8301
8331
  }
8332
+ function contactListFilterFromUrl(url) {
8333
+ return {
8334
+ ...url.searchParams.get("company_id") ? { company_id: url.searchParams.get("company_id") } : {},
8335
+ ...url.searchParams.get("status") ? { status: url.searchParams.get("status") } : {},
8336
+ ...url.searchParams.get("tag_id") ? { tag_id: url.searchParams.get("tag_id") } : {},
8337
+ ...url.searchParams.get("q") ? { q: url.searchParams.get("q") } : {},
8338
+ ...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
8339
+ ...url.searchParams.get("offset") ? { offset: Number(url.searchParams.get("offset")) } : {}
8340
+ };
8341
+ }
8302
8342
  async function handleV1Request(req, url) {
8303
8343
  const path = url.pathname;
8304
8344
  if (path !== "/v1" && !path.startsWith("/v1/"))
@@ -8412,13 +8452,7 @@ async function handleV1Request(req, url) {
8412
8452
  if (resource === "contacts") {
8413
8453
  if (!id) {
8414
8454
  if (method === "GET") {
8415
- const result = await store.listContacts({
8416
- ...url.searchParams.get("company_id") ? { company_id: url.searchParams.get("company_id") } : {},
8417
- ...url.searchParams.get("status") ? { status: url.searchParams.get("status") } : {},
8418
- ...url.searchParams.get("q") ? { q: url.searchParams.get("q") } : {},
8419
- ...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
8420
- ...url.searchParams.get("offset") ? { offset: Number(url.searchParams.get("offset")) } : {}
8421
- });
8455
+ const result = await store.listContacts(contactListFilterFromUrl(url));
8422
8456
  return json(result);
8423
8457
  }
8424
8458
  if (method === "POST") {
@@ -9016,6 +9016,14 @@ var init_pg_migrations = __esm(() => {
9016
9016
  CREATE INDEX IF NOT EXISTS idx_contacts_tombstones_deleted_at ON _contacts_tombstones(deleted_at);
9017
9017
 
9018
9018
  INSERT INTO _migrations (version) VALUES (12) ON CONFLICT DO NOTHING;
9019
+ `,
9020
+ `
9021
+ -- The contact_tags primary key is (contact_id, tag_id), which does not
9022
+ -- support the public tag_id filter efficiently. Keep this forward-only and
9023
+ -- idempotent for populated cloud deployments.
9024
+ CREATE INDEX IF NOT EXISTS idx_contact_tags_tag_contact ON contact_tags(tag_id, contact_id);
9025
+
9026
+ INSERT INTO _migrations (version) VALUES (13) ON CONFLICT DO NOTHING;
9019
9027
  `
9020
9028
  ];
9021
9029
  });
@@ -31351,6 +31359,19 @@ class ContactsPgStore {
31351
31359
  constructor(client) {
31352
31360
  this.client = client;
31353
31361
  }
31362
+ async attachTags(contacts) {
31363
+ const tagsByContactId = new Map(contacts.map((contact) => [contact.id, []]));
31364
+ if (contacts.length === 0)
31365
+ return [];
31366
+ const rows = await this.client.many(`SELECT ct.contact_id, t.*
31367
+ FROM contact_tags ct
31368
+ JOIN tags t ON t.id = ct.tag_id
31369
+ WHERE ct.contact_id = ANY($1::text[])
31370
+ ORDER BY t.name ASC`, [contacts.map((contact) => contact.id)]);
31371
+ for (const row of rows)
31372
+ tagsByContactId.get(row.contact_id)?.push(mapTag(row));
31373
+ return contacts.map((contact) => ({ ...contact, tags: tagsByContactId.get(contact.id) ?? [] }));
31374
+ }
31354
31375
  async listContacts(filter = {}) {
31355
31376
  const limit = Math.min(Math.max(filter.limit ?? 50, 1), 500);
31356
31377
  const offset = Math.max(filter.offset ?? 0, 0);
@@ -31364,6 +31385,10 @@ class ContactsPgStore {
31364
31385
  params.push(filter.status);
31365
31386
  where.push(`status = $${params.length}`);
31366
31387
  }
31388
+ if (filter.tag_id) {
31389
+ params.push(filter.tag_id);
31390
+ where.push(`EXISTS (SELECT 1 FROM contact_tags ct WHERE ct.contact_id = contacts.id AND ct.tag_id = $${params.length})`);
31391
+ }
31367
31392
  if (filter.q) {
31368
31393
  params.push(filter.q);
31369
31394
  where.push(`search_vector @@ plainto_tsquery('simple', $${params.length})`);
@@ -31372,11 +31397,13 @@ class ContactsPgStore {
31372
31397
  const countRow = await this.client.get(`SELECT COUNT(*)::text AS count FROM contacts ${whereSql}`, params);
31373
31398
  params.push(limit, offset);
31374
31399
  const rows = await this.client.many(`SELECT * FROM contacts ${whereSql} ORDER BY display_name ASC LIMIT $${params.length - 1} OFFSET $${params.length}`, params);
31375
- return { contacts: rows.map(mapContact), count: Number(countRow?.count ?? rows.length) };
31400
+ return { contacts: await this.attachTags(rows.map(mapContact)), count: Number(countRow?.count ?? rows.length) };
31376
31401
  }
31377
31402
  async getContact(id) {
31378
31403
  const row = await this.client.get(`SELECT * FROM contacts WHERE id = $1`, [id]);
31379
- return row ? mapContact(row) : null;
31404
+ if (!row)
31405
+ return null;
31406
+ return (await this.attachTags([mapContact(row)]))[0];
31380
31407
  }
31381
31408
  async createContact(input) {
31382
31409
  const id = uuid3();
@@ -31412,7 +31439,7 @@ class ContactsPgStore {
31412
31439
  input.priority ?? 3,
31413
31440
  input.timezone ?? null
31414
31441
  ]);
31415
- return mapContact(row);
31442
+ return (await this.attachTags([mapContact(row)]))[0];
31416
31443
  }
31417
31444
  async updateContact(id, input) {
31418
31445
  const allowed = {};
@@ -31452,7 +31479,7 @@ class ContactsPgStore {
31452
31479
  const sets = keys.map((k, i) => `${k} = $${i + 2}`);
31453
31480
  sets.push(`updated_at = NOW()`);
31454
31481
  const row = await this.client.get(`UPDATE contacts SET ${sets.join(", ")} WHERE id = $1 RETURNING *`, [id, ...keys.map((k) => allowed[k])]);
31455
- return row ? mapContact(row) : null;
31482
+ return row ? (await this.attachTags([mapContact(row)]))[0] : null;
31456
31483
  }
31457
31484
  async deleteContact(id) {
31458
31485
  const result = await this.client.query(`DELETE FROM contacts WHERE id = $1`, [id]);
@@ -32914,6 +32941,16 @@ async function readJson(req) {
32914
32941
  return null;
32915
32942
  }
32916
32943
  }
32944
+ function contactListFilterFromUrl(url) {
32945
+ return {
32946
+ ...url.searchParams.get("company_id") ? { company_id: url.searchParams.get("company_id") } : {},
32947
+ ...url.searchParams.get("status") ? { status: url.searchParams.get("status") } : {},
32948
+ ...url.searchParams.get("tag_id") ? { tag_id: url.searchParams.get("tag_id") } : {},
32949
+ ...url.searchParams.get("q") ? { q: url.searchParams.get("q") } : {},
32950
+ ...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
32951
+ ...url.searchParams.get("offset") ? { offset: Number(url.searchParams.get("offset")) } : {}
32952
+ };
32953
+ }
32917
32954
  async function handleV1Request(req, url) {
32918
32955
  const path = url.pathname;
32919
32956
  if (path !== "/v1" && !path.startsWith("/v1/"))
@@ -33027,13 +33064,7 @@ async function handleV1Request(req, url) {
33027
33064
  if (resource === "contacts") {
33028
33065
  if (!id) {
33029
33066
  if (method === "GET") {
33030
- const result = await store.listContacts({
33031
- ...url.searchParams.get("company_id") ? { company_id: url.searchParams.get("company_id") } : {},
33032
- ...url.searchParams.get("status") ? { status: url.searchParams.get("status") } : {},
33033
- ...url.searchParams.get("q") ? { q: url.searchParams.get("q") } : {},
33034
- ...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
33035
- ...url.searchParams.get("offset") ? { offset: Number(url.searchParams.get("offset")) } : {}
33036
- });
33067
+ const result = await store.listContacts(contactListFilterFromUrl(url));
33037
33068
  return json5(result);
33038
33069
  }
33039
33070
  if (method === "POST") {
@@ -33693,6 +33724,7 @@ function getPackageVersion() {
33693
33724
  // src/server/openapi.ts
33694
33725
  var contactSchema = {
33695
33726
  type: "object",
33727
+ required: ["tags"],
33696
33728
  properties: {
33697
33729
  id: { type: "string" },
33698
33730
  first_name: { type: "string" },
@@ -33707,6 +33739,7 @@ var contactSchema = {
33707
33739
  sensitivity: { type: "string" },
33708
33740
  archived: { type: "boolean" },
33709
33741
  priority: { type: "number" },
33742
+ tags: { type: "array", items: { $ref: "#/components/schemas/Tag" } },
33710
33743
  created_at: { type: "string" },
33711
33744
  updated_at: { type: "string" }
33712
33745
  }
@@ -33839,6 +33872,7 @@ function buildV1OpenApiDocument(version2 = getPackageVersion()) {
33839
33872
  { name: "q", in: "query", schema: { type: "string" } },
33840
33873
  { name: "company_id", in: "query", schema: { type: "string" } },
33841
33874
  { name: "status", in: "query", schema: { type: "string" } },
33875
+ { name: "tag_id", in: "query", schema: { type: "string" } },
33842
33876
  { name: "limit", in: "query", schema: { type: "number" } },
33843
33877
  { name: "offset", in: "query", schema: { type: "number" } }
33844
33878
  ],
@@ -0,0 +1,15 @@
1
+ -- @hasna/contacts cloud schema (A1 pure-remote) — generated from src/db/pg-migrations.ts
2
+ -- Idempotent and forward-only: retains deletion metadata for remote sync.
3
+
4
+ CREATE TABLE IF NOT EXISTS _contacts_tombstones (
5
+ table_name TEXT NOT NULL CHECK(table_name IN ('contacts','companies','tags')),
6
+ row_id TEXT NOT NULL,
7
+ deleted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
8
+ actor TEXT,
9
+ reason TEXT,
10
+ PRIMARY KEY (table_name, row_id)
11
+ );
12
+
13
+ CREATE INDEX IF NOT EXISTS idx_contacts_tombstones_deleted_at ON _contacts_tombstones(deleted_at);
14
+
15
+ INSERT INTO _migrations (version) VALUES (12) ON CONFLICT DO NOTHING;
@@ -0,0 +1,9 @@
1
+ -- @hasna/contacts cloud schema (A1 pure-remote) — generated from src/db/pg-migrations.ts
2
+ -- Idempotent and forward-only: supports GET /v1/contacts?tag_id=... filters.
3
+
4
+ -- The contact_tags primary key is (contact_id, tag_id), which does not
5
+ -- support the public tag_id filter efficiently. Keep this forward-only and
6
+ -- idempotent for populated cloud deployments.
7
+ CREATE INDEX IF NOT EXISTS idx_contact_tags_tag_contact ON contact_tags(tag_id, contact_id);
8
+
9
+ INSERT INTO _migrations (version) VALUES (13) ON CONFLICT DO NOTHING;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/contacts",
3
- "version": "0.6.33",
3
+ "version": "0.6.34",
4
4
  "description": "Contact management for AI coding agents — CLI + MCP + Web",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1 +0,0 @@
1
- /*! tailwindcss v4.2.2 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-duration:initial;--tw-space-x-reverse:0}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-50:oklch(97.1% .013 17.38);--color-red-200:oklch(88.5% .062 18.334);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-red-950:oklch(25.8% .092 26.042);--color-yellow-50:oklch(98.7% .026 102.212);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-800:oklch(47.6% .114 61.907);--color-yellow-950:oklch(28.6% .066 53.813);--color-green-50:oklch(98.2% .018 155.826);--color-green-200:oklch(92.5% .084 155.995);--color-green-800:oklch(44.8% .119 151.328);--color-green-900:oklch(39.3% .095 152.535);--color-green-950:oklch(26.6% .065 152.934);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--animate-spin:spin 1s linear infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:root{--background:0 0% 100%;--foreground:222.2 84% 4.9%;--card:0 0% 100%;--card-foreground:222.2 84% 4.9%;--popover:0 0% 100%;--popover-foreground:222.2 84% 4.9%;--primary:221.2 83.2% 53.3%;--primary-foreground:210 40% 98%;--secondary:210 40% 96.1%;--secondary-foreground:222.2 47.4% 11.2%;--muted:210 40% 96.1%;--muted-foreground:215.4 16.3% 46.9%;--accent:210 40% 96.1%;--accent-foreground:222.2 47.4% 11.2%;--destructive:0 84.2% 60.2%;--destructive-foreground:210 40% 98%;--border:214.3 31.8% 91.4%;--input:214.3 31.8% 91.4%;--ring:221.2 83.2% 53.3%;--radius:.5rem}.dark{--background:222.2 84% 4.9%;--foreground:210 40% 98%;--card:222.2 84% 4.9%;--card-foreground:210 40% 98%;--popover:222.2 84% 4.9%;--popover-foreground:210 40% 98%;--primary:217.2 91.2% 59.8%;--primary-foreground:222.2 47.4% 11.2%;--secondary:217.2 32.6% 17.5%;--secondary-foreground:210 40% 98%;--muted:217.2 32.6% 17.5%;--muted-foreground:215 20.2% 65.1%;--accent:217.2 32.6% 17.5%;--accent-foreground:210 40% 98%;--destructive:0 62.8% 30.6%;--destructive-foreground:210 40% 98%;--border:217.2 32.6% 17.5%;--input:217.2 32.6% 17.5%;--ring:224.3 76.3% 48%}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground));min-height:100vh;margin:0;font-family:system-ui,-apple-system,sans-serif}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:calc(var(--spacing) * 0)}.end{inset-inline-end:var(--spacing)}.top-4{top:calc(var(--spacing) * 4)}.top-\[50\%\]{top:50%}.right-1{right:calc(var(--spacing) * 1)}.right-2{right:calc(var(--spacing) * 2)}.right-4{right:calc(var(--spacing) * 4)}.right-6{right:calc(var(--spacing) * 6)}.bottom-6{bottom:calc(var(--spacing) * 6)}.left-2{left:calc(var(--spacing) * 2)}.left-3{left:calc(var(--spacing) * 3)}.left-\[50\%\]{left:50%}.z-50{z-index:50}.-mx-1{margin-inline:calc(var(--spacing) * -1)}.mx-auto{margin-inline:auto}.my-1{margin-block:calc(var(--spacing) * 1)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mr-1{margin-right:calc(var(--spacing) * 1)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-2{margin-left:calc(var(--spacing) * 2)}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-flex{display:inline-flex}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.size-9{width:calc(var(--spacing) * 9);height:calc(var(--spacing) * 9)}.size-16{width:calc(var(--spacing) * 16);height:calc(var(--spacing) * 16)}.h-3\.5{height:calc(var(--spacing) * 3.5)}.h-4{height:calc(var(--spacing) * 4)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-14{height:calc(var(--spacing) * 14)}.h-\[calc\(100vh-56px\)\]{height:calc(100vh - 56px)}.h-\[var\(--radix-select-trigger-height\)\]{height:var(--radix-select-trigger-height)}.h-full{height:100%}.h-px{height:1px}.max-h-96{max-height:calc(var(--spacing) * 96)}.max-h-\[90vh\]{max-height:90vh}.min-h-screen{min-height:100vh}.w-3\.5{width:calc(var(--spacing) * 3.5)}.w-4{width:calc(var(--spacing) * 4)}.w-9{width:calc(var(--spacing) * 9)}.w-80{width:calc(var(--spacing) * 80)}.w-full{width:100%}.max-w-7xl{max-width:var(--container-7xl)}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-xl{max-width:var(--container-xl)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-\[8rem\]{min-width:8rem}.min-w-\[var\(--radix-select-trigger-width\)\]{min-width:var(--radix-select-trigger-width)}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.caption-bottom{caption-side:bottom}.translate-x-\[-50\%\]{--tw-translate-x:-50%;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-1{--tw-translate-y:calc(var(--spacing) * 1);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-y-\[-50\%\]{--tw-translate-y:-50%;translate:var(--tw-translate-x) var(--tw-translate-y)}.scale-0{--tw-scale-x:0%;--tw-scale-y:0%;--tw-scale-z:0%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-100{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y)}.scale-110{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}.rotate-0{rotate:0deg}.rotate-90{rotate:90deg}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-green-200{border-color:var(--color-green-200)}.border-red-200{border-color:var(--color-red-200)}.border-transparent{border-color:#0000}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab,red,red)){.bg-black\/50{background-color:color-mix(in oklab,var(--color-black) 50%,transparent)}}.bg-green-50{background-color:var(--color-green-50)}.bg-red-50{background-color:var(--color-red-50)}.bg-transparent{background-color:#0000}.bg-yellow-50{background-color:var(--color-yellow-50)}.p-1{padding:calc(var(--spacing) * 1)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0{padding-block:calc(var(--spacing) * 0)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-0{padding-top:calc(var(--spacing) * 0)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pr-8{padding-right:calc(var(--spacing) * 8)}.pr-9{padding-right:calc(var(--spacing) * 9)}.pl-2{padding-left:calc(var(--spacing) * 2)}.pl-6{padding-left:calc(var(--spacing) * 6)}.pl-8{padding-left:calc(var(--spacing) * 8)}.pl-9{padding-left:calc(var(--spacing) * 9)}.text-center{text-align:center}.text-left{text-align:left}.align-middle{vertical-align:middle}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.leading-none{--tw-leading:1;line-height:1}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-green-800{color:var(--color-green-800)}.text-red-800{color:var(--color-red-800)}.text-white{color:var(--color-white)}.text-yellow-800{color:var(--color-yellow-800)}.capitalize{text-transform:capitalize}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.underline-offset-4{text-underline-offset:4px}.opacity-0{opacity:0}.opacity-30{opacity:.3}.opacity-50{opacity:.5}.opacity-70{opacity:.7}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}@media(hover:hover){.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}}.file\:border-0::file-selector-button{border-style:var(--tw-border-style);border-width:0}.file\:bg-transparent::file-selector-button{background-color:#0000}.file\:text-sm::file-selector-button{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.file\:font-medium::file-selector-button{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.last\:border-0:last-child{border-style:var(--tw-border-style);border-width:0}@media(hover:hover){.hover\:underline:hover{text-decoration-line:underline}.hover\:opacity-80:hover{opacity:.8}.hover\:opacity-100:hover{opacity:1}}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px;--tw-ring-offset-shadow:var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-1:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:outline-none:focus-visible{--tw-outline-style:none;outline-style:none}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.data-\[disabled\]\:pointer-events-none[data-disabled]{pointer-events:none}.data-\[disabled\]\:opacity-50[data-disabled]{opacity:.5}@media(min-width:40rem){.sm\:flex-row{flex-direction:row}.sm\:justify-end{justify-content:flex-end}:where(.sm\:space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)))}.sm\:rounded-lg{border-radius:var(--radius-lg)}.sm\:text-left{text-align:left}}@media(min-width:48rem){.md\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}@media(prefers-color-scheme:dark){.dark\:scale-0{--tw-scale-x:0%;--tw-scale-y:0%;--tw-scale-z:0%;scale:var(--tw-scale-x) var(--tw-scale-y)}.dark\:scale-100{--tw-scale-x:100%;--tw-scale-y:100%;--tw-scale-z:100%;scale:var(--tw-scale-x) var(--tw-scale-y)}.dark\:-rotate-90{rotate:-90deg}.dark\:rotate-0{rotate:0deg}.dark\:border-green-900{border-color:var(--color-green-900)}.dark\:border-red-900{border-color:var(--color-red-900)}.dark\:bg-green-950{background-color:var(--color-green-950)}.dark\:bg-red-950{background-color:var(--color-red-950)}.dark\:bg-yellow-950{background-color:var(--color-yellow-950)}.dark\:text-green-200{color:var(--color-green-200)}.dark\:text-red-200{color:var(--color-red-200)}.dark\:text-yellow-200{color:var(--color-yellow-200)}}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:size-4 svg{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}.\[\&_tr\]\:border-b tr{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.\[\&_tr\:last-child\]\:border-0 tr:last-child{border-style:var(--tw-border-style);border-width:0}.\[\&\:has\(\[role\=checkbox\]\)\]\:pr-0:has([role=checkbox]){padding-right:calc(var(--spacing) * 0)}.\[\&\>\[role\=checkbox\]\]\:translate-y-\[2px\]>[role=checkbox]{--tw-translate-y:2px;translate:var(--tw-translate-x) var(--tw-translate-y)}.\[\&\>span\]\:line-clamp-1>span{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.\[\&\>svg\]\:size-4>svg{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.\[\&\>svg\]\:shrink-0>svg{flex-shrink:0}.\[\&\>tr\]\:last\:border-b-0>tr:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-duration{syntax:"*";inherits:false}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@keyframes spin{to{transform:rotate(360deg)}}