@singi-labs/sifa-sdk 0.19.59 → 0.19.61

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 (49) hide show
  1. package/dist/import/presentation-csv.js +12 -1
  2. package/dist/import/presentation-csv.js.map +1 -1
  3. package/dist/{index-CT-A3BEe.d.ts → index-IeuOpcIB.d.ts} +24 -1
  4. package/dist/{index-BeQbepFn.d.cts → index-syVBnrbI.d.cts} +24 -1
  5. package/dist/index.cjs +13 -2
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +8 -6
  8. package/dist/index.d.ts +8 -6
  9. package/dist/index.js +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/dist/jsonld/index.d.cts +1 -1
  12. package/dist/jsonld/index.d.ts +1 -1
  13. package/dist/{network-map-DHOOMSCP.d.cts → network-map-Bw_rIDtH.d.cts} +3 -3
  14. package/dist/{network-map-CKF7Yozc.d.ts → network-map-DUam4K3f.d.ts} +3 -3
  15. package/dist/{org-CelDIOiN.d.ts → org--5MN6Ev4.d.ts} +15 -3
  16. package/dist/{org-Bq3HbpLD.d.cts → org-B_0EMNuF.d.cts} +15 -3
  17. package/dist/{profile-summary-CD3-QflX.d.cts → profile-summary-CU4sWBzF.d.cts} +1 -1
  18. package/dist/{profile-summary-CyRzFe7f.d.ts → profile-summary-RT-roUtM.d.ts} +1 -1
  19. package/dist/query/fetchers/confirmations.js +16 -0
  20. package/dist/query/fetchers/confirmations.js.map +1 -1
  21. package/dist/query/fetchers/index.cjs +16 -0
  22. package/dist/query/fetchers/index.cjs.map +1 -1
  23. package/dist/query/fetchers/index.d.cts +6 -6
  24. package/dist/query/fetchers/index.d.ts +6 -6
  25. package/dist/query/fetchers/index.js +2 -0
  26. package/dist/query/fetchers/index.js.map +1 -1
  27. package/dist/query/hooks/index.cjs.map +1 -1
  28. package/dist/query/hooks/index.d.cts +4 -4
  29. package/dist/query/hooks/index.d.ts +4 -4
  30. package/dist/query/index.cjs +16 -0
  31. package/dist/query/index.cjs.map +1 -1
  32. package/dist/query/index.d.cts +9 -9
  33. package/dist/query/index.d.ts +9 -9
  34. package/dist/query/index.js +2 -0
  35. package/dist/query/index.js.map +1 -1
  36. package/dist/resume/index.d.cts +1 -1
  37. package/dist/resume/index.d.ts +1 -1
  38. package/dist/schemas/confirmation.js +8 -1
  39. package/dist/schemas/confirmation.js.map +1 -1
  40. package/dist/schemas/index.cjs +8 -1
  41. package/dist/schemas/index.cjs.map +1 -1
  42. package/dist/schemas/index.d.cts +1 -1
  43. package/dist/schemas/index.d.ts +1 -1
  44. package/dist/types/index.js.map +1 -1
  45. package/dist/{types-BHnGSJ3z.d.cts → types-BV-qBo9i.d.cts} +1 -1
  46. package/dist/{types-BKEBVobU.d.ts → types-Jb4KKbn9.d.ts} +1 -1
  47. package/dist/{use-org-notification-emails-BTR2Yxq8.d.cts → use-org-notification-emails-B8e4vgs-.d.cts} +3 -3
  48. package/dist/{use-org-notification-emails-DDIi6giD.d.ts → use-org-notification-emails-CbQ3Kz96.d.ts} +3 -3
  49. package/package.json +1 -1
@@ -2,8 +2,19 @@
2
2
  function clean(value) {
3
3
  return (value ?? "").trim();
4
4
  }
5
+ var DURATION_PART = /(\d{1,5}(?:[.,]\d{1,3})?)\s{0,3}(h(?:ours?|rs?)?|m(?:in(?:ute)?s?)?)?(?![a-z])/gi;
6
+ var DURATION_MAX_LENGTH = 100;
5
7
  function parsePresentationDuration(input) {
6
- const numbers = (clean(input).match(/\d+/g) ?? []).map((n) => Number.parseInt(n, 10)).filter((n) => Number.isInteger(n) && n > 0);
8
+ const parts = [...clean(input).slice(0, DURATION_MAX_LENGTH).matchAll(DURATION_PART)].map(
9
+ (m) => ({
10
+ value: Number.parseFloat((m[1] ?? "").replace(",", ".")),
11
+ unit: m[2]?.toLowerCase().startsWith("h") ? "h" : m[2] ? "m" : void 0
12
+ })
13
+ );
14
+ const numbers = parts.map((part, i) => {
15
+ const unit = part.unit ?? parts.slice(i + 1).find((p) => p.unit)?.unit ?? "m";
16
+ return Math.round(unit === "h" ? part.value * 60 : part.value);
17
+ }).filter((n) => Number.isInteger(n) && n > 0);
7
18
  const min = numbers.at(0);
8
19
  if (min === void 0) return void 0;
9
20
  const max = numbers.at(1);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/import/presentation-csv.ts"],"sourcesContent":["import type {\n ProfilePresentationRecord,\n ProfilePresentationDeliveryRecord,\n PresentationLink,\n PresentationDuration,\n} from '../schemas/index.js';\n\n/**\n * CSV parsers for the Talks & sessions import (two templates joined by a\n * presentation_key). The value parsers and row mappers are reusable and pure;\n * the actual PDS writing (create a presentation, then write its deliveries with\n * a presentationRef to it) is orchestrated by the consumer, which has the\n * created record's AT-URI.\n */\n\n/** A CSV row as a header-keyed map. Missing cells may be undefined or empty. */\nexport type CsvRow = Record<string, string | undefined>;\n\nfunction clean(value: string | undefined): string {\n return (value ?? '').trim();\n}\n\n/**\n * Parse a free-text duration into minutes: a single value (\"30 min\", \"30\n * minutes\", \"30\") or a range (\"20-30 min\", \"20 to 30 minutes\", \"20-30\").\n * Returns undefined when no usable number is present. A second number is used\n * as the upper bound only when it is greater than or equal to the first.\n */\nexport function parsePresentationDuration(\n input: string | undefined,\n): PresentationDuration | undefined {\n const numbers = (clean(input).match(/\\d+/g) ?? [])\n .map((n) => Number.parseInt(n, 10))\n .filter((n) => Number.isInteger(n) && n > 0);\n // `.at()` is typed `number | undefined`, which both tsc (under\n // noUncheckedIndexedAccess) and eslint agree on, so the guards are clean.\n const min = numbers.at(0);\n if (min === undefined) return undefined;\n const max = numbers.at(1);\n return max !== undefined && max >= min\n ? { minMinutes: min, maxMinutes: max }\n : { minMinutes: min };\n}\n\n/** Build a duration from explicit min/max minute columns. */\nexport function durationFromMinutes(\n minRaw: string | undefined,\n maxRaw: string | undefined,\n): PresentationDuration | undefined {\n const min = Number.parseInt(clean(minRaw), 10);\n if (!Number.isInteger(min) || min < 1) return undefined;\n const max = Number.parseInt(clean(maxRaw), 10);\n return Number.isInteger(max) && max >= min\n ? { minMinutes: min, maxMinutes: max }\n : { minMinutes: min };\n}\n\n/** Split a delimited list into trimmed, non-empty values. Default delimiter \";\". */\nexport function parseIntendedAudiences(input: string | undefined, delimiter = ';'): string[] {\n return clean(input)\n .split(delimiter)\n .map((s) => s.trim())\n .filter(Boolean);\n}\n\n/**\n * Strip HTML to plain text and decode the common entities, for descriptions\n * exported as HTML (e.g. Webflow). The AppView re-sanitizes on write; this is\n * only to produce readable text.\n */\nexport function stripHtmlToText(input: string | undefined): string {\n const text = clean(input)\n .replace(/<\\s*br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/\\s*(p|div|li|h[1-6])\\s*>/gi, '\\n')\n .replace(/<[^>]*>/g, '')\n .replace(/&nbsp;/gi, ' ')\n .replace(/&amp;/gi, '&')\n .replace(/&lt;/gi, '<')\n .replace(/&gt;/gi, '>')\n .replace(/&quot;/gi, '\"')\n .replace(/&#0*39;|&apos;/gi, \"'\");\n return text\n .replace(/[ \\t]+/g, ' ')\n .replace(/ *\\n */g, '\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n\nconst ROLE_TOKENS: Record<string, string> = {\n presenter: 'id.sifa.defs#presenter',\n speaker: 'id.sifa.defs#presenter',\n panelist: 'id.sifa.defs#panelist',\n keynote: 'id.sifa.defs#keynote',\n workshop: 'id.sifa.defs#workshop',\n host: 'id.sifa.defs#host',\n};\n\n/**\n * Map a friendly role value to its token. An existing token passes through.\n * Free-text or compound values (e.g. \"Organizer & co-host/moderator\") are\n * mapped to the nearest token by keyword. An organizer-only value has no\n * speaking token, so it, and any other unrecognized value, is dropped rather\n * than stored raw -- matching normalizePresentationMode's drop-unknown\n * behavior, so a fixed role dropdown never has to render arbitrary strings.\n */\nexport function normalizePresentationRole(value: string | undefined): string | undefined {\n const v = clean(value).toLowerCase();\n if (!v) return undefined;\n if (v.startsWith('id.sifa.defs#')) return v;\n const exact = ROLE_TOKENS[v];\n if (exact) return exact;\n if (v.includes('keynote')) return ROLE_TOKENS.keynote;\n if (v.includes('workshop') || v.includes('masterclass') || v.includes('training'))\n return ROLE_TOKENS.workshop;\n if (v.includes('panel')) return ROLE_TOKENS.panelist;\n if (v.includes('host') || v.includes('moderat') || v.includes('emcee') || v.includes('chair'))\n return ROLE_TOKENS.host;\n if (v.includes('speaker') || v.includes('present') || v.includes('talk'))\n return ROLE_TOKENS.presenter;\n return undefined;\n}\n\nconst MODE_FRAGMENTS: Record<string, string> = {\n inperson: '#inperson',\n 'in person': '#inperson',\n 'in-person': '#inperson',\n virtual: '#virtual',\n online: '#virtual',\n remote: '#virtual',\n hybrid: '#hybrid',\n};\n\n/** Map a friendly mode value to the community calendar token; drops an unknown value. */\nexport function normalizePresentationMode(value: string | undefined): string | undefined {\n const v = clean(value).toLowerCase();\n if (!v) return undefined;\n if (v.startsWith('community.lexicon.calendar.event#')) return v;\n const fragment = MODE_FRAGMENTS[v];\n return fragment ? `community.lexicon.calendar.event${fragment}` : undefined;\n}\n\nfunction link(uri: string | undefined, type: string): PresentationLink | undefined {\n const u = clean(uri);\n return u ? { uri: u, type } : undefined;\n}\n\nexport interface ParsedPresentation {\n /** presentation_key used to link delivery rows to this presentation. */\n key?: string;\n record: Omit<ProfilePresentationRecord, 'createdAt'>;\n}\n\n/** Map a row of the presentations template to a presentation record (minus createdAt). */\nexport function presentationCsvRowToRecord(row: CsvRow): ParsedPresentation {\n const description = stripHtmlToText(row.description);\n const duration =\n parsePresentationDuration(row.duration) ??\n durationFromMinutes(row.duration_min_minutes, row.duration_max_minutes);\n const intendedAudiences = parseIntendedAudiences(row.intended_audiences);\n const links = [\n link(row.slides_url, 'id.sifa.defs#linkSlides'),\n link(row.recording_url, 'id.sifa.defs#linkRecording'),\n link(row.writeup_url, 'id.sifa.defs#linkWriteup'),\n ].filter((l): l is PresentationLink => l !== undefined);\n\n const record: Omit<ProfilePresentationRecord, 'createdAt'> = { title: clean(row.title) };\n if (description) record.description = description;\n if (duration) record.duration = duration;\n if (intendedAudiences.length) record.intendedAudiences = intendedAudiences;\n if (links.length) record.links = links;\n\n return { key: clean(row.presentation_key) || undefined, record };\n}\n\nexport interface ParsedDelivery {\n /** presentation_key to resolve into a presentationRef once the presentation exists. */\n presentationKey?: string;\n record: Omit<ProfilePresentationDeliveryRecord, 'createdAt' | 'presentationRef'>;\n}\n\n/** Map a row of the deliveries template to a delivery record (minus createdAt and presentationRef). */\nexport function presentationDeliveryCsvRowToRecord(row: CsvRow): ParsedDelivery {\n const links = [\n link(row.event_url, 'id.sifa.defs#linkEvent'),\n link(row.recording_url, 'id.sifa.defs#linkRecording'),\n ].filter((l): l is PresentationLink => l !== undefined);\n\n const record: Omit<ProfilePresentationDeliveryRecord, 'createdAt' | 'presentationRef'> = {};\n const title = clean(row.title);\n const eventName = clean(row.event_name);\n const date = clean(row.date);\n const location = clean(row.location);\n const role = normalizePresentationRole(row.role);\n const mode = normalizePresentationMode(row.mode);\n if (title) record.title = title;\n if (role) record.role = role;\n if (eventName) record.eventName = eventName;\n if (date) record.date = date;\n if (location) record.location = location;\n if (mode) record.mode = mode;\n if (links.length) record.links = links;\n\n return { presentationKey: clean(row.presentation_key) || undefined, record };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/import/presentation-csv.ts"],"sourcesContent":["import type {\n ProfilePresentationRecord,\n ProfilePresentationDeliveryRecord,\n PresentationLink,\n PresentationDuration,\n} from '../schemas/index.js';\n\n/**\n * CSV parsers for the Talks & sessions import (two templates joined by a\n * presentation_key). The value parsers and row mappers are reusable and pure;\n * the actual PDS writing (create a presentation, then write its deliveries with\n * a presentationRef to it) is orchestrated by the consumer, which has the\n * created record's AT-URI.\n */\n\n/** A CSV row as a header-keyed map. Missing cells may be undefined or empty. */\nexport type CsvRow = Record<string, string | undefined>;\n\nfunction clean(value: string | undefined): string {\n return (value ?? '').trim();\n}\n\n/**\n * A number with an optional unit: \"30\", \"30 min\", \"1.5 h\", \"3 hours\". The\n * lookahead stops a unit from matching the start of a word (\"20 to 30\").\n */\nconst DURATION_PART =\n /(\\d{1,5}(?:[.,]\\d{1,3})?)\\s{0,3}(h(?:ours?|rs?)?|m(?:in(?:ute)?s?)?)?(?![a-z])/gi;\n\n/** Durations are short free text; cap the input so matching stays linear. */\nconst DURATION_MAX_LENGTH = 100;\n\n/**\n * Parse a free-text duration into minutes: a single value (\"30 min\", \"30\n * minutes\", \"30\", \"3 hours\") or a range (\"20-30 min\", \"20 to 30 minutes\",\n * \"40 minutes - 4 hours\"). Hours convert to minutes. A number without a unit\n * takes the unit of the next number (\"2-3 hours\" is 120 to 180), else minutes.\n * Returns undefined when no usable number is present. A second number is used\n * as the upper bound only when it is greater than or equal to the first.\n */\nexport function parsePresentationDuration(\n input: string | undefined,\n): PresentationDuration | undefined {\n const parts = [...clean(input).slice(0, DURATION_MAX_LENGTH).matchAll(DURATION_PART)].map(\n (m) => ({\n value: Number.parseFloat((m[1] ?? '').replace(',', '.')),\n unit: m[2]?.toLowerCase().startsWith('h') ? 'h' : m[2] ? 'm' : undefined,\n }),\n );\n const numbers = parts\n .map((part, i) => {\n const unit = part.unit ?? parts.slice(i + 1).find((p) => p.unit)?.unit ?? 'm';\n return Math.round(unit === 'h' ? part.value * 60 : part.value);\n })\n .filter((n) => Number.isInteger(n) && n > 0);\n // `.at()` is typed `number | undefined`, which both tsc (under\n // noUncheckedIndexedAccess) and eslint agree on, so the guards are clean.\n const min = numbers.at(0);\n if (min === undefined) return undefined;\n const max = numbers.at(1);\n return max !== undefined && max >= min\n ? { minMinutes: min, maxMinutes: max }\n : { minMinutes: min };\n}\n\n/** Build a duration from explicit min/max minute columns. */\nexport function durationFromMinutes(\n minRaw: string | undefined,\n maxRaw: string | undefined,\n): PresentationDuration | undefined {\n const min = Number.parseInt(clean(minRaw), 10);\n if (!Number.isInteger(min) || min < 1) return undefined;\n const max = Number.parseInt(clean(maxRaw), 10);\n return Number.isInteger(max) && max >= min\n ? { minMinutes: min, maxMinutes: max }\n : { minMinutes: min };\n}\n\n/** Split a delimited list into trimmed, non-empty values. Default delimiter \";\". */\nexport function parseIntendedAudiences(input: string | undefined, delimiter = ';'): string[] {\n return clean(input)\n .split(delimiter)\n .map((s) => s.trim())\n .filter(Boolean);\n}\n\n/**\n * Strip HTML to plain text and decode the common entities, for descriptions\n * exported as HTML (e.g. Webflow). The AppView re-sanitizes on write; this is\n * only to produce readable text.\n */\nexport function stripHtmlToText(input: string | undefined): string {\n const text = clean(input)\n .replace(/<\\s*br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/\\s*(p|div|li|h[1-6])\\s*>/gi, '\\n')\n .replace(/<[^>]*>/g, '')\n .replace(/&nbsp;/gi, ' ')\n .replace(/&amp;/gi, '&')\n .replace(/&lt;/gi, '<')\n .replace(/&gt;/gi, '>')\n .replace(/&quot;/gi, '\"')\n .replace(/&#0*39;|&apos;/gi, \"'\");\n return text\n .replace(/[ \\t]+/g, ' ')\n .replace(/ *\\n */g, '\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n\nconst ROLE_TOKENS: Record<string, string> = {\n presenter: 'id.sifa.defs#presenter',\n speaker: 'id.sifa.defs#presenter',\n panelist: 'id.sifa.defs#panelist',\n keynote: 'id.sifa.defs#keynote',\n workshop: 'id.sifa.defs#workshop',\n host: 'id.sifa.defs#host',\n};\n\n/**\n * Map a friendly role value to its token. An existing token passes through.\n * Free-text or compound values (e.g. \"Organizer & co-host/moderator\") are\n * mapped to the nearest token by keyword. An organizer-only value has no\n * speaking token, so it, and any other unrecognized value, is dropped rather\n * than stored raw -- matching normalizePresentationMode's drop-unknown\n * behavior, so a fixed role dropdown never has to render arbitrary strings.\n */\nexport function normalizePresentationRole(value: string | undefined): string | undefined {\n const v = clean(value).toLowerCase();\n if (!v) return undefined;\n if (v.startsWith('id.sifa.defs#')) return v;\n const exact = ROLE_TOKENS[v];\n if (exact) return exact;\n if (v.includes('keynote')) return ROLE_TOKENS.keynote;\n if (v.includes('workshop') || v.includes('masterclass') || v.includes('training'))\n return ROLE_TOKENS.workshop;\n if (v.includes('panel')) return ROLE_TOKENS.panelist;\n if (v.includes('host') || v.includes('moderat') || v.includes('emcee') || v.includes('chair'))\n return ROLE_TOKENS.host;\n if (v.includes('speaker') || v.includes('present') || v.includes('talk'))\n return ROLE_TOKENS.presenter;\n return undefined;\n}\n\nconst MODE_FRAGMENTS: Record<string, string> = {\n inperson: '#inperson',\n 'in person': '#inperson',\n 'in-person': '#inperson',\n virtual: '#virtual',\n online: '#virtual',\n remote: '#virtual',\n hybrid: '#hybrid',\n};\n\n/** Map a friendly mode value to the community calendar token; drops an unknown value. */\nexport function normalizePresentationMode(value: string | undefined): string | undefined {\n const v = clean(value).toLowerCase();\n if (!v) return undefined;\n if (v.startsWith('community.lexicon.calendar.event#')) return v;\n const fragment = MODE_FRAGMENTS[v];\n return fragment ? `community.lexicon.calendar.event${fragment}` : undefined;\n}\n\nfunction link(uri: string | undefined, type: string): PresentationLink | undefined {\n const u = clean(uri);\n return u ? { uri: u, type } : undefined;\n}\n\nexport interface ParsedPresentation {\n /** presentation_key used to link delivery rows to this presentation. */\n key?: string;\n record: Omit<ProfilePresentationRecord, 'createdAt'>;\n}\n\n/** Map a row of the presentations template to a presentation record (minus createdAt). */\nexport function presentationCsvRowToRecord(row: CsvRow): ParsedPresentation {\n const description = stripHtmlToText(row.description);\n const duration =\n parsePresentationDuration(row.duration) ??\n durationFromMinutes(row.duration_min_minutes, row.duration_max_minutes);\n const intendedAudiences = parseIntendedAudiences(row.intended_audiences);\n const links = [\n link(row.slides_url, 'id.sifa.defs#linkSlides'),\n link(row.recording_url, 'id.sifa.defs#linkRecording'),\n link(row.writeup_url, 'id.sifa.defs#linkWriteup'),\n ].filter((l): l is PresentationLink => l !== undefined);\n\n const record: Omit<ProfilePresentationRecord, 'createdAt'> = { title: clean(row.title) };\n if (description) record.description = description;\n if (duration) record.duration = duration;\n if (intendedAudiences.length) record.intendedAudiences = intendedAudiences;\n if (links.length) record.links = links;\n\n return { key: clean(row.presentation_key) || undefined, record };\n}\n\nexport interface ParsedDelivery {\n /** presentation_key to resolve into a presentationRef once the presentation exists. */\n presentationKey?: string;\n record: Omit<ProfilePresentationDeliveryRecord, 'createdAt' | 'presentationRef'>;\n}\n\n/** Map a row of the deliveries template to a delivery record (minus createdAt and presentationRef). */\nexport function presentationDeliveryCsvRowToRecord(row: CsvRow): ParsedDelivery {\n const links = [\n link(row.event_url, 'id.sifa.defs#linkEvent'),\n link(row.recording_url, 'id.sifa.defs#linkRecording'),\n ].filter((l): l is PresentationLink => l !== undefined);\n\n const record: Omit<ProfilePresentationDeliveryRecord, 'createdAt' | 'presentationRef'> = {};\n const title = clean(row.title);\n const eventName = clean(row.event_name);\n const date = clean(row.date);\n const location = clean(row.location);\n const role = normalizePresentationRole(row.role);\n const mode = normalizePresentationMode(row.mode);\n if (title) record.title = title;\n if (role) record.role = role;\n if (eventName) record.eventName = eventName;\n if (date) record.date = date;\n if (location) record.location = location;\n if (mode) record.mode = mode;\n if (links.length) record.links = links;\n\n return { presentationKey: clean(row.presentation_key) || undefined, record };\n}\n"],"mappings":"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","names":[]}
@@ -71,7 +71,30 @@ interface ProfilePosition {
71
71
  linkedSkills?: ProfileSkill[];
72
72
  primary?: boolean;
73
73
  hidden?: boolean;
74
+ /**
75
+ * Colleagues who confirmed this position (`id.sifa.defs#colleague`), as
76
+ * resolved by the AppView. Descriptive only: who said so, never a score.
77
+ * Absent when nobody has confirmed it.
78
+ */
79
+ confirmations?: PositionConfirmations;
80
+ /**
81
+ * What the signed-in viewer can do with this position. `available` means
82
+ * they have a reason to know it (an overlapping position at the same
83
+ * organization, or they are the organization's account) and have not
84
+ * confirmed it yet; `confirmed` means they already did and can withdraw.
85
+ * Absent for anonymous viewers, the owner, and anyone without a reason to know.
86
+ */
87
+ viewerConfirmation?: PositionViewerConfirmation;
88
+ }
89
+ /** Confirmations of one position, as returned by the AppView. */
90
+ interface PositionConfirmations {
91
+ /** Number of visible confirmers. Equal to `confirmers.length`. */
92
+ count: number;
93
+ /** The confirmers, oldest first. Blocked and suppressed accounts are left out. */
94
+ confirmers: ActorCard[];
74
95
  }
96
+ /** See {@link ProfilePosition.viewerConfirmation}. */
97
+ type PositionViewerConfirmation = 'available' | 'confirmed';
75
98
  interface ProfileEducation {
76
99
  rkey: string;
77
100
  institution: string;
@@ -963,4 +986,4 @@ interface Profile {
963
986
  externalAccounts?: ExternalAccount[];
964
987
  }
965
988
 
966
- export { type AccountFacetMode as A, type ProfileInvestmentLink as B, type CoSpeaker as C, type ProfileInvolvementLink as D, type Endorsement as E, type FeedItem as F, type ProfileLocation as G, type ProfileOverrideSource as H, type ProfilePresentation as I, type ProfileVolunteering as J, type ProjectMemberCard as K, type LocationValue as L, type ProjectRole as M, type PublicationContributor as N, type OrgAddressView as O, type ProfileSkill as P, type RecognizedEntity as Q, type RelatedIdentifier as R, type SkillRef as S, type SkillSuggestion as T, type TrustStat as U, type VerifiedAccount as V, type ProfilePresentationDelivery as a, type PdsProviderInfo as b, type Profile as c, type ProfilePosition as d, type ProfileCertification as e, type ProfileEducation as f, type ProfileHonor as g, type ProfileLanguage as h, type ProfileProject as i, type ProfilePublication as j, type ProfileInvolvement as k, type ActiveApp as l, type ActorCard as m, type AuthorSuggestion as n, type EndorsementData as o, type ExternalAccount as p, type ExternalAccountKeytraceClaim as q, type LanguageProficiency as r, type OrgFloorVerdict as s, type OrgLinkView as t, type OrgProfileView as u, PROJECT_ROLES as v, type PresentationLinkView as w, type ProfileCourse as x, type ProfileIndustry as y, type ProfileInvestment as z };
989
+ export { type AccountFacetMode as A, type ProfileIndustry as B, type CoSpeaker as C, type ProfileInvestment as D, type Endorsement as E, type FeedItem as F, type ProfileInvestmentLink as G, type ProfileInvolvementLink as H, type ProfileLocation as I, type ProfileOverrideSource as J, type ProfilePresentation as K, type LocationValue as L, type ProfileVolunteering as M, type ProjectMemberCard as N, type OrgAddressView as O, type ProfileSkill as P, type ProjectRole as Q, type RelatedIdentifier as R, type PublicationContributor as S, type RecognizedEntity as T, type SkillRef as U, type SkillSuggestion as V, type TrustStat as W, type VerifiedAccount as X, type ProfilePresentationDelivery as a, type PdsProviderInfo as b, type Profile as c, type ProfilePosition as d, type ProfileCertification as e, type ProfileEducation as f, type ProfileHonor as g, type ProfileLanguage as h, type ProfileProject as i, type ProfilePublication as j, type ProfileInvolvement as k, type ActiveApp as l, type ActorCard as m, type AuthorSuggestion as n, type EndorsementData as o, type ExternalAccount as p, type ExternalAccountKeytraceClaim as q, type LanguageProficiency as r, type OrgFloorVerdict as s, type OrgLinkView as t, type OrgProfileView as u, PROJECT_ROLES as v, type PositionConfirmations as w, type PositionViewerConfirmation as x, type PresentationLinkView as y, type ProfileCourse as z };
@@ -71,7 +71,30 @@ interface ProfilePosition {
71
71
  linkedSkills?: ProfileSkill[];
72
72
  primary?: boolean;
73
73
  hidden?: boolean;
74
+ /**
75
+ * Colleagues who confirmed this position (`id.sifa.defs#colleague`), as
76
+ * resolved by the AppView. Descriptive only: who said so, never a score.
77
+ * Absent when nobody has confirmed it.
78
+ */
79
+ confirmations?: PositionConfirmations;
80
+ /**
81
+ * What the signed-in viewer can do with this position. `available` means
82
+ * they have a reason to know it (an overlapping position at the same
83
+ * organization, or they are the organization's account) and have not
84
+ * confirmed it yet; `confirmed` means they already did and can withdraw.
85
+ * Absent for anonymous viewers, the owner, and anyone without a reason to know.
86
+ */
87
+ viewerConfirmation?: PositionViewerConfirmation;
88
+ }
89
+ /** Confirmations of one position, as returned by the AppView. */
90
+ interface PositionConfirmations {
91
+ /** Number of visible confirmers. Equal to `confirmers.length`. */
92
+ count: number;
93
+ /** The confirmers, oldest first. Blocked and suppressed accounts are left out. */
94
+ confirmers: ActorCard[];
74
95
  }
96
+ /** See {@link ProfilePosition.viewerConfirmation}. */
97
+ type PositionViewerConfirmation = 'available' | 'confirmed';
75
98
  interface ProfileEducation {
76
99
  rkey: string;
77
100
  institution: string;
@@ -963,4 +986,4 @@ interface Profile {
963
986
  externalAccounts?: ExternalAccount[];
964
987
  }
965
988
 
966
- export { type AccountFacetMode as A, type ProfileInvestmentLink as B, type CoSpeaker as C, type ProfileInvolvementLink as D, type Endorsement as E, type FeedItem as F, type ProfileLocation as G, type ProfileOverrideSource as H, type ProfilePresentation as I, type ProfileVolunteering as J, type ProjectMemberCard as K, type LocationValue as L, type ProjectRole as M, type PublicationContributor as N, type OrgAddressView as O, type ProfileSkill as P, type RecognizedEntity as Q, type RelatedIdentifier as R, type SkillRef as S, type SkillSuggestion as T, type TrustStat as U, type VerifiedAccount as V, type ProfilePresentationDelivery as a, type PdsProviderInfo as b, type Profile as c, type ProfilePosition as d, type ProfileCertification as e, type ProfileEducation as f, type ProfileHonor as g, type ProfileLanguage as h, type ProfileProject as i, type ProfilePublication as j, type ProfileInvolvement as k, type ActiveApp as l, type ActorCard as m, type AuthorSuggestion as n, type EndorsementData as o, type ExternalAccount as p, type ExternalAccountKeytraceClaim as q, type LanguageProficiency as r, type OrgFloorVerdict as s, type OrgLinkView as t, type OrgProfileView as u, PROJECT_ROLES as v, type PresentationLinkView as w, type ProfileCourse as x, type ProfileIndustry as y, type ProfileInvestment as z };
989
+ export { type AccountFacetMode as A, type ProfileIndustry as B, type CoSpeaker as C, type ProfileInvestment as D, type Endorsement as E, type FeedItem as F, type ProfileInvestmentLink as G, type ProfileInvolvementLink as H, type ProfileLocation as I, type ProfileOverrideSource as J, type ProfilePresentation as K, type LocationValue as L, type ProfileVolunteering as M, type ProjectMemberCard as N, type OrgAddressView as O, type ProfileSkill as P, type ProjectRole as Q, type RelatedIdentifier as R, type PublicationContributor as S, type RecognizedEntity as T, type SkillRef as U, type SkillSuggestion as V, type TrustStat as W, type VerifiedAccount as X, type ProfilePresentationDelivery as a, type PdsProviderInfo as b, type Profile as c, type ProfilePosition as d, type ProfileCertification as e, type ProfileEducation as f, type ProfileHonor as g, type ProfileLanguage as h, type ProfileProject as i, type ProfilePublication as j, type ProfileInvolvement as k, type ActiveApp as l, type ActorCard as m, type AuthorSuggestion as n, type EndorsementData as o, type ExternalAccount as p, type ExternalAccountKeytraceClaim as q, type LanguageProficiency as r, type OrgFloorVerdict as s, type OrgLinkView as t, type OrgProfileView as u, PROJECT_ROLES as v, type PositionConfirmations as w, type PositionViewerConfirmation as x, type PresentationLinkView as y, type ProfileCourse as z };
package/dist/index.cjs CHANGED
@@ -3553,8 +3553,19 @@ function meetsContrastAA(foreground, background) {
3553
3553
  function clean2(value) {
3554
3554
  return (value ?? "").trim();
3555
3555
  }
3556
+ var DURATION_PART = /(\d{1,5}(?:[.,]\d{1,3})?)\s{0,3}(h(?:ours?|rs?)?|m(?:in(?:ute)?s?)?)?(?![a-z])/gi;
3557
+ var DURATION_MAX_LENGTH = 100;
3556
3558
  function parsePresentationDuration(input) {
3557
- const numbers = (clean2(input).match(/\d+/g) ?? []).map((n) => Number.parseInt(n, 10)).filter((n) => Number.isInteger(n) && n > 0);
3559
+ const parts = [...clean2(input).slice(0, DURATION_MAX_LENGTH).matchAll(DURATION_PART)].map(
3560
+ (m) => ({
3561
+ value: Number.parseFloat((m[1] ?? "").replace(",", ".")),
3562
+ unit: m[2]?.toLowerCase().startsWith("h") ? "h" : m[2] ? "m" : void 0
3563
+ })
3564
+ );
3565
+ const numbers = parts.map((part, i) => {
3566
+ const unit = part.unit ?? parts.slice(i + 1).find((p) => p.unit)?.unit ?? "m";
3567
+ return Math.round(unit === "h" ? part.value * 60 : part.value);
3568
+ }).filter((n) => Number.isInteger(n) && n > 0);
3558
3569
  const min = numbers.at(0);
3559
3570
  if (min === void 0) return void 0;
3560
3571
  const max = numbers.at(1);
@@ -7186,7 +7197,7 @@ function describeSifaRecord(collection, value) {
7186
7197
  }
7187
7198
 
7188
7199
  // src/index.ts
7189
- var SIFA_SDK_VERSION = "0.19.59";
7200
+ var SIFA_SDK_VERSION = "0.19.61";
7190
7201
 
7191
7202
  exports.ACTIVITY_TIERS = ACTIVITY_TIERS;
7192
7203
  exports.ACTIVITY_VERBS = ACTIVITY_VERBS;