@dereekb/dbx-components-mcp 14.13.0 → 14.15.0

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.
@@ -10,7 +10,7 @@ var __export = (target, all) => {
10
10
  // packages/dbx-components-mcp/package.json
11
11
  var package_default = {
12
12
  name: "@dereekb/dbx-components-mcp",
13
- version: "14.13.0",
13
+ version: "14.15.0",
14
14
  sideEffects: false,
15
15
  type: "module",
16
16
  bin: {
@@ -22,8 +22,8 @@ var package_default = {
22
22
  "ts-morph": "^21.0.0"
23
23
  },
24
24
  peerDependencies: {
25
- "@dereekb/dbx-cli": "14.13.0",
26
- "@dereekb/util": "14.13.0"
25
+ "@dereekb/dbx-cli": "14.15.0",
26
+ "@dereekb/util": "14.15.0"
27
27
  }
28
28
  };
29
29
 
@@ -2496,10 +2496,18 @@ var FIREBASE_MODELS = [
2496
2496
  tsType: "Maybe<NotificationHealthCheck>",
2497
2497
  optional: true,
2498
2498
  description: "The result of the most recent notification delivery health check run for this user."
2499
+ },
2500
+ {
2501
+ name: "tso",
2502
+ longName: "textStoppedPhoneNumbers",
2503
+ converter: "optionalFirestoreArray<E164PhoneNumber>({ filterUnique: true, dontStoreIfEmpty: true })",
2504
+ tsType: "Maybe<E164PhoneNumber[]>",
2505
+ optional: true,
2506
+ description: "Phone numbers that replied STOP to a text. Texts to a number in this list resolve to off, at send time and in the health check."
2499
2507
  }
2500
2508
  ],
2501
2509
  enums: [],
2502
- detectionHints: ["b", "x", "dc", "gc", "bc", "ns", "hc"],
2510
+ detectionHints: ["b", "x", "dc", "gc", "bc", "ns", "hc", "tso"],
2503
2511
  description: "A global notification user profile that tracks notification preferences and box subscriptions.",
2504
2512
  modelGroup: "Notification",
2505
2513
  collectionKind: "root",
@@ -6492,15 +6500,16 @@ function parseSignalInput(property) {
6492
6500
  ...defaultValue === void 0 ? {} : { defaultValue }
6493
6501
  };
6494
6502
  }
6503
+ var SIGNAL_INPUT_FUNCTION_NAMES = /* @__PURE__ */ new Set(["input", "model"]);
6495
6504
  function classifySignalInputCall(call) {
6496
6505
  const expression = call.getExpression();
6497
6506
  let result;
6498
- if (Node9.isIdentifier(expression) && expression.getText() === "input") {
6507
+ if (Node9.isIdentifier(expression) && SIGNAL_INPUT_FUNCTION_NAMES.has(expression.getText())) {
6499
6508
  result = "plain";
6500
6509
  } else if (Node9.isPropertyAccessExpression(expression)) {
6501
6510
  const baseExpr = expression.getExpression();
6502
6511
  const propertyName = expression.getName();
6503
- if (Node9.isIdentifier(baseExpr) && baseExpr.getText() === "input" && propertyName === "required") {
6512
+ if (Node9.isIdentifier(baseExpr) && SIGNAL_INPUT_FUNCTION_NAMES.has(baseExpr.getText()) && propertyName === "required") {
6504
6513
  result = "required";
6505
6514
  }
6506
6515
  }
@@ -24747,9 +24756,9 @@ var RULE_CATALOG = [
24747
24756
  {
24748
24757
  code: "NOTIF_TASK_IN_VALIDATE_WITHOUT_HANDLER",
24749
24758
  source: "dbx_notification_m_validate_app",
24750
- severity: "warning",
24759
+ severity: "error",
24751
24760
  title: "A task type appears in `validate:` but has no matching `handlers:` registration",
24752
- whatItFlags: "A task type appears in `validate:` but has no matching `handlers:` registration.",
24761
+ whatItFlags: "A task type appears in `validate:` but has no matching `handlers:` registration.\n`notificationTaskService()` throws at startup for this.",
24753
24762
  whenItApplies: "When `notificationTaskService({ validate, handlers })` lists a task type in `validate:` that the `handlers:` traversal doesn't reach.",
24754
24763
  whenItDoesNotApply: "Tasks whose handler is provided by an upstream module (covered by the trust list).",
24755
24764
  canonicalFix: "Add the matching `NotificationTaskServiceTaskHandlerConfig` for the task and include it in `handlers:`."
@@ -24870,6 +24879,22 @@ var RULE_CATALOG = [
24870
24879
  whenItDoesNotApply: "Spreads from trust-listed `@dereekb/*` modules.",
24871
24880
  canonicalFix: "Declare the missing function or import it from a trusted module."
24872
24881
  },
24882
+ {
24883
+ code: "NOTIF_TEMPLATE_FACTORY_UNLISTED_DELIVERY_METHOD",
24884
+ source: "dbx_notification_m_validate_app",
24885
+ severity: "warning",
24886
+ title: "A template's message factory returns channel content for a delivery method its `NotificationTemplateTypeInfo` does not list in `userConfigurableDeliveryMethods`",
24887
+ whatItFlags: "A template's message factory returns channel content for a delivery method its `NotificationTemplateTypeInfo` does not list in `userConfigurableDeliveryMethods`.",
24888
+ whenItApplies: "Template types whose info declares an explicit `userConfigurableDeliveryMethods` array and whose handler factory (or the function its `factory:` names) builds an object literal with `emailContent`, `textContent` or `notificationSummaryContent` for a method the array leaves out.",
24889
+ whenItDoesNotApply: "Infos without `userConfigurableDeliveryMethods` (the defaults list email, text and notification summary), lists the scanner cannot read statically, and content built outside the factory's own source \u2014 the send pipeline's runtime warning (`notificationMessageFunctionWithUnlistedDeliveryMethodsWarning`) still covers those.",
24890
+ canonicalFix: "Stop returning the unlisted content from the factory, or add the delivery method to the info's `userConfigurableDeliveryMethods`.",
24891
+ seeAlso: [
24892
+ {
24893
+ kind: "artifact",
24894
+ target: "notification-template"
24895
+ }
24896
+ ]
24897
+ },
24873
24898
  {
24874
24899
  code: "NOTIF_TEMPLATE_INFO_MISSING",
24875
24900
  source: "dbx_notification_m_validate_app",
@@ -31566,7 +31591,8 @@ __export(notification_m_validate_app_exports, {
31566
31591
  extractAppNotifications: () => extractAppNotifications,
31567
31592
  formatResult: () => formatResult6,
31568
31593
  inspectAppNotifications: () => inspectAppNotifications,
31569
- validateAppNotifications: () => validateAppNotifications
31594
+ validateAppNotifications: () => validateAppNotifications,
31595
+ validateExtractedAppNotifications: () => validateExtractedAppNotifications
31570
31596
  });
31571
31597
 
31572
31598
  // packages/dbx-cli/validate/src/lib/notification-m-validate-app/extract.ts
@@ -31587,6 +31613,25 @@ var TEMPLATE_CONFIGS_ARRAY_FACTORY_SUFFIX = "NotificationTemplateServiceConfigsA
31587
31613
  var TEMPLATE_CONFIGS_ARRAY_TOKEN = "NOTIFICATION_TEMPLATE_SERVICE_CONFIGS_ARRAY_TOKEN";
31588
31614
  var CHECKPOINT_ALIAS_SUFFIX = "NotificationTaskCheckpoint";
31589
31615
  var TASK_DATA_INTERFACE_SUFFIX = "NotificationTaskData";
31616
+ var USER_CONFIGURABLE_DELIVERY_METHODS_PROPERTY = "userConfigurableDeliveryMethods";
31617
+ var DELIVERY_METHOD_ENUM = "NotificationDeliveryMethod";
31618
+ var DELIVERY_METHOD_BY_MEMBER = /* @__PURE__ */ new Map([
31619
+ ["EMAIL", "EMAIL"],
31620
+ ["TEXT", "TEXT"],
31621
+ ["PUSH", "PUSH"],
31622
+ ["NOTIFICATION_SUMMARY", "NOTIFICATION_SUMMARY"]
31623
+ ]);
31624
+ var DELIVERY_METHOD_BY_CODE = /* @__PURE__ */ new Map([
31625
+ ["e", "EMAIL"],
31626
+ ["t", "TEXT"],
31627
+ ["p", "PUSH"],
31628
+ ["n", "NOTIFICATION_SUMMARY"]
31629
+ ]);
31630
+ var CONTENT_PROPERTY_DELIVERY_METHODS = /* @__PURE__ */ new Map([
31631
+ ["emailContent", "EMAIL"],
31632
+ ["textContent", "TEXT"],
31633
+ ["notificationSummaryContent", "NOTIFICATION_SUMMARY"]
31634
+ ]);
31590
31635
  function extractAppNotifications(inspection) {
31591
31636
  const { componentSources, apiSources } = buildInMemoryProject(inspection);
31592
31637
  const trustedExternalIdentifiers = collectTrustedExternalIdentifiers([...componentSources, ...apiSources]);
@@ -31660,13 +31705,16 @@ function extractTemplateTypeInfos(sources) {
31660
31705
  if (!nameMatches && !typeMatches) continue;
31661
31706
  const obj = asObjectLiteral(decl.getInitializer());
31662
31707
  if (!obj) continue;
31708
+ const deliveryMethods = readUserConfigurableDeliveryMethods(obj, sf);
31663
31709
  const entry = {
31664
31710
  symbolName: name,
31665
31711
  typeConstantName: readIdentifierProperty(obj, "type"),
31666
31712
  humanName: readStringProperty2(obj, "name"),
31667
31713
  description: readStringProperty2(obj, "description"),
31668
- notificationMIdentity: readIdentifierProperty(obj, "notificationMIdentity"),
31714
+ notificationModelIdentity: readIdentifierProperty(obj, "notificationModelIdentity"),
31669
31715
  targetModelIdentity: readIdentifierProperty(obj, "targetModelIdentity"),
31716
+ userConfigurableDeliveryMethods: deliveryMethods.methods,
31717
+ userConfigurableDeliveryMethodsSource: deliveryMethods.source,
31670
31718
  sourceFile: rel,
31671
31719
  line: decl.getStartLineNumber()
31672
31720
  };
@@ -31676,6 +31724,44 @@ function extractTemplateTypeInfos(sources) {
31676
31724
  }
31677
31725
  return out;
31678
31726
  }
31727
+ function readUserConfigurableDeliveryMethods(obj, sf) {
31728
+ const value = unwrapAsExpressions(getPropertyInitializer(obj, USER_CONFIGURABLE_DELIVERY_METHODS_PROPERTY));
31729
+ let result;
31730
+ if (!value || isNullishLiteral(value)) {
31731
+ result = { methods: void 0, source: "default" };
31732
+ } else if (Node28.isIdentifier(value)) {
31733
+ result = readDeliveryMethodsArray(asArrayLiteral(findLocalVariable(sf, value.getText())?.getInitializer()));
31734
+ } else {
31735
+ result = readDeliveryMethodsArray(asArrayLiteral(value));
31736
+ }
31737
+ return result;
31738
+ }
31739
+ function readDeliveryMethodsArray(arr) {
31740
+ const methods = [];
31741
+ let readable = arr !== void 0;
31742
+ for (const el of arr?.getElements() ?? []) {
31743
+ const method = readDeliveryMethodElement(unwrapAsExpressions(el));
31744
+ if (!method) {
31745
+ readable = false;
31746
+ } else if (!methods.includes(method)) {
31747
+ methods.push(method);
31748
+ }
31749
+ }
31750
+ const result = readable ? { methods, source: "declared" } : { methods: void 0, source: "unresolved" };
31751
+ return result;
31752
+ }
31753
+ function readDeliveryMethodElement(el) {
31754
+ let result;
31755
+ if (el && Node28.isPropertyAccessExpression(el) && el.getExpression().getText() === DELIVERY_METHOD_ENUM) {
31756
+ result = DELIVERY_METHOD_BY_MEMBER.get(el.getName());
31757
+ } else if (el && Node28.isStringLiteral(el)) {
31758
+ result = DELIVERY_METHOD_BY_CODE.get(el.getLiteralText());
31759
+ }
31760
+ return result;
31761
+ }
31762
+ function isNullishLiteral(node) {
31763
+ return node !== void 0 && (Node28.isIdentifier(node) && node.getText() === "undefined" || node.getKind() === SyntaxKind12.NullKeyword);
31764
+ }
31679
31765
  function extractTemplateInfoAggregates(sources) {
31680
31766
  const out = [];
31681
31767
  for (const sf of sources) {
@@ -32037,6 +32123,7 @@ function collectFromSingleEntryFactory(name, out, index) {
32037
32123
  out.push({
32038
32124
  typeIdentifier: typeIdent,
32039
32125
  factoryFunctionName: name,
32126
+ contentDeliveryMethods: collectContentDeliveryMethods([fn.node, factoryPropertyFunctionNode(obj, index)]),
32040
32127
  sourceFile: fn.relPath,
32041
32128
  line: obj.getStartLineNumber()
32042
32129
  });
@@ -32063,7 +32150,7 @@ function consumeArrayEntryFactoryElement(options) {
32063
32150
  chaseDirectSingleEntryFactoryCall(inner, out, index);
32064
32151
  return;
32065
32152
  }
32066
- recordInlineEntryFactoryObject({ inner, factoryName, relPath, out });
32153
+ recordInlineEntryFactoryObject({ inner, factoryName, relPath, out, index });
32067
32154
  }
32068
32155
  function chaseSpreadEntryFactoryCall(inner, out, index) {
32069
32156
  if (!Node28.isCallExpression(inner)) return;
@@ -32080,12 +32167,40 @@ function chaseDirectSingleEntryFactoryCall(inner, out, index) {
32080
32167
  }
32081
32168
  }
32082
32169
  function recordInlineEntryFactoryObject(options) {
32083
- const { inner, factoryName, relPath, out } = options;
32170
+ const { inner, factoryName, relPath, out, index } = options;
32084
32171
  const obj = asObjectLiteral(inner);
32085
32172
  if (!obj) return;
32086
32173
  const typeIdent = readIdentifierProperty(obj, "type");
32087
32174
  if (!typeIdent) return;
32088
- out.push({ typeIdentifier: typeIdent, factoryFunctionName: factoryName, sourceFile: relPath, line: obj.getStartLineNumber() });
32175
+ const contentDeliveryMethods = collectContentDeliveryMethods([obj, factoryPropertyFunctionNode(obj, index)]);
32176
+ out.push({ typeIdentifier: typeIdent, factoryFunctionName: factoryName, contentDeliveryMethods, sourceFile: relPath, line: obj.getStartLineNumber() });
32177
+ }
32178
+ function factoryPropertyFunctionNode(obj, index) {
32179
+ const name = readIdentifierProperty(obj, "factory");
32180
+ return name ? index.functionsByName.get(name)?.node : void 0;
32181
+ }
32182
+ function collectContentDeliveryMethods(nodes) {
32183
+ const found = /* @__PURE__ */ new Set();
32184
+ for (const node of nodes) {
32185
+ if (node) {
32186
+ scanContentDeliveryMethods(node, found);
32187
+ }
32188
+ }
32189
+ return Array.from(CONTENT_PROPERTY_DELIVERY_METHODS.values()).filter((method) => found.has(method));
32190
+ }
32191
+ function scanContentDeliveryMethods(node, found) {
32192
+ for (const prop of node.getDescendantsOfKind(SyntaxKind12.PropertyAssignment)) {
32193
+ const method = CONTENT_PROPERTY_DELIVERY_METHODS.get(prop.getName());
32194
+ if (method && !isNullishLiteral(unwrapAsExpressions(prop.getInitializer()))) {
32195
+ found.add(method);
32196
+ }
32197
+ }
32198
+ for (const prop of node.getDescendantsOfKind(SyntaxKind12.ShorthandPropertyAssignment)) {
32199
+ const method = CONTENT_PROPERTY_DELIVERY_METHODS.get(prop.getName());
32200
+ if (method) {
32201
+ found.add(method);
32202
+ }
32203
+ }
32089
32204
  }
32090
32205
  function extractTaskServiceCalls(sources, index, trustedExternal) {
32091
32206
  const out = [];
@@ -32373,6 +32488,7 @@ function runRules6(inspection, extracted) {
32373
32488
  checkTemplateInfoPairing(extracted, violations);
32374
32489
  checkTemplateInfoRecord(extracted, violations);
32375
32490
  checkTemplateHandlerFactory(extracted, violations);
32491
+ checkTemplateDeliveryMethods(extracted, violations);
32376
32492
  checkTasks(extracted, violations);
32377
32493
  checkDuplicates2(extracted, violations);
32378
32494
  return violations;
@@ -32541,6 +32657,27 @@ function checkTemplateHandlerFactory(extracted, violations) {
32541
32657
  });
32542
32658
  }
32543
32659
  }
32660
+ function checkTemplateDeliveryMethods(extracted, violations) {
32661
+ const infoByTypeConstant = /* @__PURE__ */ new Map();
32662
+ for (const info of extracted.templateTypeInfos) {
32663
+ if (info.typeConstantName) infoByTypeConstant.set(info.typeConstantName, info);
32664
+ }
32665
+ for (const entry of extracted.templateHandlerEntries) {
32666
+ const info = infoByTypeConstant.get(entry.typeIdentifier);
32667
+ const listed = info?.userConfigurableDeliveryMethodsSource === "declared" ? info.userConfigurableDeliveryMethods : void 0;
32668
+ if (!listed) continue;
32669
+ const unlisted = entry.contentDeliveryMethods.filter((method) => !listed.includes(method));
32670
+ if (unlisted.length === 0) continue;
32671
+ const listedText = listed.length > 0 ? listed.join(", ") : "none";
32672
+ pushViolation8(violations, {
32673
+ code: "NOTIF_TEMPLATE_FACTORY_UNLISTED_DELIVERY_METHOD",
32674
+ severity: "warning",
32675
+ message: `Template handler factory \`${entry.factoryFunctionName ?? "<anonymous>"}\` for \`${entry.typeIdentifier}\` builds content for ${unlisted.join(", ")}, but \`${info?.symbolName}\` only lists ${listedText} in \`userConfigurableDeliveryMethods\`. This is the build-time counterpart of the send pipeline's runtime unlisted-delivery-method warning.`,
32676
+ side: "api",
32677
+ file: entry.sourceFile
32678
+ });
32679
+ }
32680
+ }
32544
32681
  function checkTasks(extracted, violations) {
32545
32682
  if (extracted.taskTypeConstants.length === 0) return;
32546
32683
  flagTasksMissingFromAllArrays(extracted, violations);
@@ -32687,8 +32824,7 @@ function flagValidateWithoutHandler(options) {
32687
32824
  if (!lenientHandlerTypeIdentifiers.has(name) && !extracted.trustedExternalIdentifiers.has(name)) {
32688
32825
  pushViolation8(violations, {
32689
32826
  code: "NOTIF_TASK_IN_VALIDATE_WITHOUT_HANDLER",
32690
- severity: "warning",
32691
- message: `Task type \`${name}\` appears in \`notificationTaskService({ validate })\` but has no matching handler in the \`handlers\` array.`,
32827
+ message: `Task type \`${name}\` appears in \`notificationTaskService({ validate })\` but has no matching handler in the \`handlers\` array. \`notificationTaskService()\` throws at startup for this.`,
32692
32828
  side: "api",
32693
32829
  file: call.sourceFile
32694
32830
  });
@@ -32874,12 +33010,11 @@ async function inspectAppNotifications(componentDir, apiDir) {
32874
33010
  }
32875
33011
 
32876
33012
  // packages/dbx-cli/validate/src/lib/notification-m-validate-app/index.ts
32877
- function validateAppNotifications(inspection, options) {
32878
- const extracted = extractAppNotifications(inspection);
33013
+ function validateExtractedAppNotifications(input) {
32879
33014
  const violations = [];
32880
33015
  let errorCount = 0;
32881
33016
  let warningCount = 0;
32882
- for (const v of runRules6(inspection, extracted)) {
33017
+ for (const v of runRules6(input.inspection, input.extracted)) {
32883
33018
  violations.push(v);
32884
33019
  if (v.severity === "error") {
32885
33020
  errorCount += 1;
@@ -32891,11 +33026,14 @@ function validateAppNotifications(inspection, options) {
32891
33026
  violations,
32892
33027
  errorCount,
32893
33028
  warningCount,
32894
- componentDir: options.componentDir,
32895
- apiDir: options.apiDir
33029
+ componentDir: input.componentDir,
33030
+ apiDir: input.apiDir
32896
33031
  };
32897
33032
  return result;
32898
33033
  }
33034
+ function validateAppNotifications(inspection, options) {
33035
+ return validateExtractedAppNotifications({ inspection, extracted: extractAppNotifications(inspection), componentDir: options.componentDir, apiDir: options.apiDir });
33036
+ }
32899
33037
 
32900
33038
  // packages/dbx-cli/validate/src/lib/notification-m-validate-folder/index.ts
32901
33039
  var notification_m_validate_folder_exports = {};
@@ -38012,6 +38150,7 @@ function listAppAssets(inspection, options) {
38012
38150
  // packages/dbx-cli/validate/src/lib/notification-m-list-app/index.ts
38013
38151
  var notification_m_list_app_exports = {};
38014
38152
  __export(notification_m_list_app_exports, {
38153
+ collectAppNotifications: () => collectAppNotifications,
38015
38154
  formatReportAsJson: () => formatReportAsJson5,
38016
38155
  formatReportAsMarkdown: () => formatReportAsMarkdown5,
38017
38156
  listAppNotifications: () => listAppNotifications
@@ -38043,8 +38182,12 @@ function buildTemplateSummaries(extracted) {
38043
38182
  const record = extracted.templateInfoRecord;
38044
38183
  const resolved = new Set(record ? record.resolvedInfoIdentifiers : []);
38045
38184
  const handlerByType = /* @__PURE__ */ new Map();
38185
+ const handlerEntriesByType = /* @__PURE__ */ new Map();
38046
38186
  for (const entry of extracted.templateHandlerEntries) {
38047
38187
  handlerByType.set(entry.typeIdentifier, { factoryFunctionName: entry.factoryFunctionName });
38188
+ const entries = handlerEntriesByType.get(entry.typeIdentifier) ?? [];
38189
+ entries.push(entry);
38190
+ handlerEntriesByType.set(entry.typeIdentifier, entries);
38048
38191
  }
38049
38192
  const templates = [];
38050
38193
  for (const constant of extracted.templateTypeConstants) {
@@ -38056,8 +38199,11 @@ function buildTemplateSummaries(extracted) {
38056
38199
  infoSymbolName: info?.symbolName,
38057
38200
  humanName: info?.humanName,
38058
38201
  description: info?.description,
38059
- notificationMIdentity: info?.notificationMIdentity,
38202
+ notificationModelIdentity: info?.notificationModelIdentity,
38060
38203
  targetModelIdentity: info?.targetModelIdentity,
38204
+ userConfigurableDeliveryMethods: info?.userConfigurableDeliveryMethods,
38205
+ userConfigurableDeliveryMethodsSource: info?.userConfigurableDeliveryMethodsSource,
38206
+ factoryContentDeliveryMethods: unionContentDeliveryMethods(handlerEntriesByType.get(constant.symbolName) ?? []),
38061
38207
  inInfoRecord: info ? resolved.has(info.symbolName) : false,
38062
38208
  hasFactory: handler !== void 0,
38063
38209
  factoryFunctionName: handler?.factoryFunctionName,
@@ -38066,6 +38212,15 @@ function buildTemplateSummaries(extracted) {
38066
38212
  }
38067
38213
  return templates;
38068
38214
  }
38215
+ function unionContentDeliveryMethods(entries) {
38216
+ const result = [];
38217
+ for (const entry of entries) {
38218
+ for (const method of entry.contentDeliveryMethods) {
38219
+ if (!result.includes(method)) result.push(method);
38220
+ }
38221
+ }
38222
+ return result;
38223
+ }
38069
38224
  function buildTaskSummaries(extracted) {
38070
38225
  const indices = buildTaskIndices(extracted);
38071
38226
  const tasks = [];
@@ -38193,13 +38348,34 @@ function formatTemplateBlock(t) {
38193
38348
  parts.push(heading);
38194
38349
  if (t.humanName) parts.push(`- Human name: ${t.humanName}`);
38195
38350
  if (t.description) parts.push(`- Description: ${t.description}`);
38196
- if (t.notificationMIdentity) parts.push(`- Notification model: \`${t.notificationMIdentity}\``);
38351
+ if (t.notificationModelIdentity) parts.push(`- Notification model: \`${t.notificationModelIdentity}\``);
38197
38352
  if (t.targetModelIdentity) parts.push(`- Target model: \`${t.targetModelIdentity}\``);
38353
+ const deliveryMethodsText = formatUserConfigurableDeliveryMethods(t);
38354
+ if (deliveryMethodsText) parts.push(`- User-configurable delivery methods: ${deliveryMethodsText}`);
38355
+ if (t.factoryContentDeliveryMethods.length > 0) parts.push(`- Factory returns content for: ${t.factoryContentDeliveryMethods.join(", ")}`);
38198
38356
  const infoText = t.infoSymbolName ? code6(t.infoSymbolName) : "_Missing._";
38199
38357
  const factorySuffix = t.factoryFunctionName ? ` (${code6(t.factoryFunctionName)})` : "";
38200
38358
  parts.push(`- Info object: ${infoText}`, `- In info record: ${formatBool2(t.inInfoRecord)}`, `- Has factory: ${formatBool2(t.hasFactory)}${factorySuffix}`, `- Source: \`${t.sourceFile}\``);
38201
38359
  return parts.join("\n");
38202
38360
  }
38361
+ function formatUserConfigurableDeliveryMethods(t) {
38362
+ let result;
38363
+ switch (t.userConfigurableDeliveryMethodsSource) {
38364
+ case "declared":
38365
+ result = t.userConfigurableDeliveryMethods && t.userConfigurableDeliveryMethods.length > 0 ? t.userConfigurableDeliveryMethods.join(", ") : "_none_";
38366
+ break;
38367
+ case "default":
38368
+ result = "default";
38369
+ break;
38370
+ case "unresolved":
38371
+ result = "_unresolved_";
38372
+ break;
38373
+ case void 0:
38374
+ result = void 0;
38375
+ break;
38376
+ }
38377
+ return result;
38378
+ }
38203
38379
  function formatTaskBlock(t) {
38204
38380
  const parts = [];
38205
38381
  const heading = t.typeCode ? `### ${t.typeCode} \u2014 \`${t.symbolName}\`` : `### \`${t.symbolName}\``;
@@ -44619,6 +44795,8 @@ var DBX_NOTIFICATION_M_VALIDATE_APP_TOOL = {
44619
44795
  "",
44620
44796
  "External identifiers imported from `@dereekb/*` are trusted \u2014 spreads and handler `type:` references that resolve into upstream packages do not produce orphan/unresolved errors.",
44621
44797
  "",
44798
+ "The same checks run at build time through `dbx-cli-generate-notification-manifest` (see the `@dereekb/dbx-cli` README); this tool always scans the current source.",
44799
+ "",
44622
44800
  "Provide both:",
44623
44801
  "- `componentDir`: relative path to the `-firebase` component package (e.g. `components/demo-firebase`).",
44624
44802
  "- `apiDir`: relative path to the API app (e.g. `apps/demo-api`).",
@@ -44655,7 +44833,9 @@ var { formatReportAsJson: formatReportAsJson11, formatReportAsMarkdown: formatRe
44655
44833
  var DBX_NOTIFICATION_M_LIST_APP_TOOL = {
44656
44834
  name: "dbx_notification_m_list_app",
44657
44835
  description: [
44658
- "List every notification template + notification task configured in a downstream `-firebase` component + API app pair. Each entry reports its type code, metadata (human name, description, notification/target model identity), and registration flags \u2014 `inInfoRecord` + `hasFactory` for templates, `inAllArray` + `inValidateList` + `hasHandler` for tasks.",
44836
+ "List every notification template + notification task configured in a downstream `-firebase` component + API app pair. Each entry reports its type code, metadata (human name, description, notification/target model identity), and registration flags \u2014 `inInfoRecord` + `hasFactory` for templates, `inAllArray` + `inValidateList` + `hasHandler` for tasks. Templates also report `userConfigurableDeliveryMethods` (with a `default` / `declared` / `unresolved` source) and `factoryContentDeliveryMethods`, the channels their factory builds content for.",
44837
+ "",
44838
+ "Its JSON is the report part of the `notification.manifest.json` that `dbx-cli-generate-notification-manifest` writes at build time; this tool always scans the current source.",
44659
44839
  "",
44660
44840
  "Cross-file resolution mirrors `dbx_notification_m_validate_app` \u2014 spreads and factory composition are traced through every `.ts` file under `src/lib/model/notification/` on the component side and `src/app/common/model/notification/` + `src/app/common/firebase/` on the API side.",
44661
44841
  "",
@@ -51420,7 +51600,7 @@ export const <<SCREAMING>>_NOTIFICATION_TEMPLATE_TYPE_INFO: NotificationTemplate
51420
51600
  type: <<SCREAMING>>_NOTIFICATION_TEMPLATE_TYPE,
51421
51601
  name: '<<Pascal>>',
51422
51602
  description: 'TODO: describe the <<Pascal>> notification.',
51423
- notificationMIdentity: profileIdentity // TODO: replace with the model identity this notification targets
51603
+ notificationModelIdentity: profileIdentity // TODO: replace with the model identity this notification targets
51424
51604
  };
51425
51605
 
51426
51606
  export interface <<Pascal>>NotificationData {
@@ -52075,6 +52255,31 @@ var STORAGEFILE_PROCESSOR_SUBTASK = {
52075
52255
  seeAlso: ["storagefile-processor", "storagefile-purpose"],
52076
52256
  verify: "Run `dbx_validate_app_storagefiles` \u2014 `STORAGEFILE_PROCESSING_SUBTASK_NOT_HANDLED` should clear once the flow entry is added."
52077
52257
  };
52258
+ var NOTIFICATION_BUILD_TIME_CHECK_STEP = {
52259
+ heading: "Build-time check",
52260
+ path: "<apiDir>/project.json",
52261
+ body: [
52262
+ "Wire `dbx-cli-generate-notification-manifest` (from `@dereekb/dbx-cli`) as an nx target so broken notification wiring fails the build before deploy. It runs the same rules as `dbx_notification_m_validate_app`, writes `notification.manifest.json`, and exits non-zero on a validator error (warnings are written into the manifest). One target covers every template and task type; add it once per app.",
52263
+ "",
52264
+ "```json",
52265
+ '"generate-notification-manifest": {',
52266
+ ' "executor": "nx:run-commands",',
52267
+ ' "outputs": ["{workspaceRoot}/dist/<apiDir>/notification.manifest.json"],',
52268
+ ' "inputs": [',
52269
+ ' "{workspaceRoot}/<componentDir>/src/lib/model/notification/**/*.ts",',
52270
+ ' "{workspaceRoot}/<apiDir>/src/app/common/model/notification/**/*.ts",',
52271
+ ' "{workspaceRoot}/<apiDir>/src/app/common/firebase/**/*.ts"',
52272
+ " ],",
52273
+ ' "options": {',
52274
+ ' "command": "npx dbx-cli-generate-notification-manifest --component-dir=<componentDir> --api-dir=<apiDir> --output=dist/<apiDir>/notification.manifest.json",',
52275
+ ' "cwd": "{workspaceRoot}"',
52276
+ " }",
52277
+ "}",
52278
+ "```",
52279
+ "",
52280
+ "**Wire into:** the API project's `build.dependsOn` (not `serve`: nothing reads the manifest at runtime, and the server's startup checks already cover `serve`)."
52281
+ ].join("\n")
52282
+ };
52078
52283
  var NOTIFICATION_TEMPLATE = {
52079
52284
  artifact: "notification-template",
52080
52285
  title: "Notification template",
@@ -52087,7 +52292,7 @@ var NOTIFICATION_TEMPLATE = {
52087
52292
  body: [
52088
52293
  "**Required exports:**",
52089
52294
  "- `<NAME>_NOTIFICATION_TEMPLATE_TYPE: NotificationTemplateType = '<short_code>'`.",
52090
- "- `<NAME>_NOTIFICATION_TEMPLATE_TYPE_INFO: NotificationTemplateTypeInfo` with `{ type, name, description, notificationMIdentity, targetModelIdentity? }`.",
52295
+ "- `<NAME>_NOTIFICATION_TEMPLATE_TYPE_INFO: NotificationTemplateTypeInfo` with `{ type, name, description, notificationModelIdentity, targetModelIdentity? }`.",
52091
52296
  "- For multi-file workspaces, also declare `ALL_<MODULE>_NOTIFICATION_TEMPLATE_TYPE_INFOS: NotificationTemplateTypeInfo[]` and re-export the per-file infos.",
52092
52297
  "",
52093
52298
  "**Barrel:** Re-export from `<componentDir>/src/lib/model/notification/index.ts`."
@@ -52109,10 +52314,11 @@ var NOTIFICATION_TEMPLATE = {
52109
52314
  "",
52110
52315
  "**Wire into:** the top-level `<app>NotificationTemplateServiceConfigsArrayFactory(context)` \u2014 call the new factory inside its returned array. The configs-array factory is itself bound via `NOTIFICATION_TEMPLATE_SERVICE_CONFIGS_ARRAY_TOKEN` in the notification module."
52111
52316
  ].join("\n")
52112
- }
52317
+ },
52318
+ NOTIFICATION_BUILD_TIME_CHECK_STEP
52113
52319
  ],
52114
52320
  seeAlso: ["notification-task"],
52115
- verify: "Run `dbx_notification_m_validate_app` to confirm both registration paths (info-record + handler factory) are wired, then `dbx_notification_m_list_app` to confirm the new template appears with `inInfoRecord: true` and `hasFactory: true`."
52321
+ verify: "Run `dbx_notification_m_validate_app` to confirm both registration paths (info-record + handler factory) are wired, then `dbx_notification_m_list_app` to confirm the new template appears with `inInfoRecord: true` and `hasFactory: true`, then `npx nx run <api-project>:generate-notification-manifest` runs the same checks the build does."
52116
52322
  };
52117
52323
  var NOTIFICATION_TASK = {
52118
52324
  artifact: "notification-task",
@@ -52152,10 +52358,11 @@ var NOTIFICATION_TASK = {
52152
52358
  {
52153
52359
  heading: "Wire into task service",
52154
52360
  body: ["Push the new handler config into the `handlers: [...]` array passed to `notificationTaskService({ validate, handlers })`. The `validate:` spread should already cover it via `ALL_NOTIFICATION_TASK_TYPES`."].join("\n")
52155
- }
52361
+ },
52362
+ NOTIFICATION_BUILD_TIME_CHECK_STEP
52156
52363
  ],
52157
52364
  seeAlso: ["notification-template"],
52158
- verify: "Run `dbx_notification_m_validate_app` to confirm `inAllArray: true` + `hasHandler: true` for the new task type."
52365
+ verify: "Run `dbx_notification_m_validate_app` to confirm `inAllArray: true` + `hasHandler: true` for the new task type, then `npx nx run <api-project>:generate-notification-manifest` runs the same checks the build does."
52159
52366
  };
52160
52367
  var NESTJS_MODEL_MODULE = {
52161
52368
  artifact: "nestjs-model-module",
@@ -123,6 +123,68 @@
123
123
  ],
124
124
  "configInterface": "DbxForgeUsernameLoginFieldUsernameConfigInput"
125
125
  },
126
+ {
127
+ "slug": "website-url",
128
+ "factoryName": "dbxForgeWebsiteUrlField",
129
+ "tier": "field-factory",
130
+ "produces": "string",
131
+ "arrayOutput": "no",
132
+ "description": "Website url input with website url validation.\n\nDefaults to the key `'website'` and label `'Website Url'` unless overridden in the config.\n\nWhen a `baseUrl` is configured (e.g. `'https://linkedin.com/in/'`), the base url is shown as a prefix and the user only types the relative path\nthat follows it (e.g. a username). Pasting a full url, or a url without the protocol, removes the base url from the input. The `valueMode`\ndetermines whether the full url or only the relative path is saved. The `requirePrefix`, `allowPorts`, and `validDomains` validation options\nonly apply when no base url is configured.",
133
+ "example": "const field = dbxForgeWebsiteUrlField();\nconst linkedInField = dbxForgeWebsiteUrlField({ key: 'linkedIn', label: 'LinkedIn', baseUrl: 'https://linkedin.com/in/' });\nconst linkedInUsernameField = dbxForgeWebsiteUrlField({ key: 'linkedInUsername', label: 'LinkedIn', baseUrl: 'https://linkedin.com/in/', valueMode: 'relative' });",
134
+ "properties": [
135
+ {
136
+ "name": "autocomplete",
137
+ "type": "FieldAutocompleteAttributeOption",
138
+ "description": "",
139
+ "required": false
140
+ },
141
+ {
142
+ "name": "notWebsiteUrlMessage",
143
+ "type": "DynamicText",
144
+ "description": "Optional override for the \"is not a valid website url\" error message.",
145
+ "required": false
146
+ },
147
+ {
148
+ "name": "notWebsiteUrlWithPrefixMessage",
149
+ "type": "DynamicText",
150
+ "description": "Optional override for the \"is not a valid website url with an http/https prefix\" error message.",
151
+ "required": false
152
+ },
153
+ {
154
+ "name": "notWebsiteUrlWithExpectedDomainMessage",
155
+ "type": "DynamicText",
156
+ "description": "Optional override for the \"is not a valid website url with an expected domain\" error message.",
157
+ "required": false
158
+ },
159
+ {
160
+ "name": "valueMode",
161
+ "type": "DbxForgeWebsiteUrlFieldValueMode",
162
+ "description": "The value saved when a base url is configured.\n\n- `'url'`: the full website url, e.g. `https://linkedin.com/in/dereekb`\n- `'relative'`: only the relative path that follows the base url, e.g. `dereekb`\n\nDefaults to `'url'`.",
163
+ "required": false
164
+ },
165
+ {
166
+ "name": "notWebsiteUrlWithExpectedBaseUrlMessage",
167
+ "type": "DynamicText",
168
+ "description": "Optional override for the \"is not a website url with the expected base url\" error message.\n\nOnly used when a base url is configured.",
169
+ "required": false
170
+ },
171
+ {
172
+ "name": "type (props)",
173
+ "type": "DbxForgeTextFieldInputType",
174
+ "description": "HTML input type. Defaults to `'text'`.",
175
+ "required": false
176
+ },
177
+ {
178
+ "name": "valueMode (props)",
179
+ "type": "DbxForgeWebsiteUrlFieldValueMode",
180
+ "description": "The value saved when a base url is configured.\n\nDefaults to `'url'`.",
181
+ "required": false
182
+ }
183
+ ],
184
+ "wrapperPattern": "unwrapped",
185
+ "ngFormType": "websiteurl",
186
+ "configInterface": "DbxForgeWebsiteUrlFieldConfig"
187
+ },
126
188
  {
127
189
  "slug": "checklist",
128
190
  "factoryName": "dbxForgeChecklistField",
@@ -584,6 +584,194 @@
584
584
  "--dbx-padding-2"
585
585
  ]
586
586
  },
587
+ {
588
+ "slug": "button-floating",
589
+ "selector": ".dbx-button-floating",
590
+ "source": "@dereekb/dbx-web",
591
+ "module": "@dereekb/dbx-web",
592
+ "file": "src/lib/button/_button.scss",
593
+ "line": 205,
594
+ "declarations": [
595
+ {
596
+ "property": "position",
597
+ "value": "sticky"
598
+ },
599
+ {
600
+ "property": "z-index",
601
+ "value": "var(--dbx-button-floating-z-index, 10)"
602
+ },
603
+ {
604
+ "property": "bottom",
605
+ "value": "$button-floating-offset-y"
606
+ },
607
+ {
608
+ "property": "display",
609
+ "value": "flex"
610
+ },
611
+ {
612
+ "property": "gap",
613
+ "value": "var(--dbx-padding-2)"
614
+ },
615
+ {
616
+ "property": "width",
617
+ "value": "fit-content"
618
+ },
619
+ {
620
+ "property": "margin-left",
621
+ "value": "auto"
622
+ },
623
+ {
624
+ "property": "--mat-button-protected-container-elevation-shadow",
625
+ "value": "var(--mat-sys-level3)"
626
+ },
627
+ {
628
+ "property": "--mat-button-protected-focus-container-elevation-shadow",
629
+ "value": "var(--mat-sys-level3)"
630
+ },
631
+ {
632
+ "property": "--mat-button-protected-pressed-container-elevation-shadow",
633
+ "value": "var(--mat-sys-level3)"
634
+ },
635
+ {
636
+ "property": "--mat-button-protected-hover-container-elevation-shadow",
637
+ "value": "var(--mat-sys-level4)"
638
+ }
639
+ ],
640
+ "role": "layout",
641
+ "intent": "float a button (or a wrapper holding several buttons) over the content with `position: sticky` — pinned `offset-y` from the bottom of its scroll container while its parent section is on screen, right-aligned by default. Raised buttons get FAB-level elevation. Compose with `.dbx-button-floating-top` / `-left` / `-center` to reposition, or `.dbx-button-floating-fixed` to pin to the viewport. Sticky breaks when an ancestor between the host and the scroller sets `overflow`. Tune with `--dbx-button-floating-offset` (or `-offset-x` / `-offset-y`) and `--dbx-button-floating-z-index`",
642
+ "seeAlso": [
643
+ "button-floating-fixed",
644
+ "button-floating-top",
645
+ "button-floating-left",
646
+ "button-floating-center",
647
+ "content-pit-floating-button"
648
+ ],
649
+ "tokensRead": [
650
+ "--dbx-button-floating-z-index",
651
+ "--dbx-padding-2",
652
+ "--mat-sys-level3",
653
+ "--mat-sys-level4"
654
+ ],
655
+ "tokensSet": [
656
+ "--mat-button-protected-container-elevation-shadow",
657
+ "--mat-button-protected-focus-container-elevation-shadow",
658
+ "--mat-button-protected-hover-container-elevation-shadow",
659
+ "--mat-button-protected-pressed-container-elevation-shadow"
660
+ ]
661
+ },
662
+ {
663
+ "slug": "button-floating-center",
664
+ "selector": ".dbx-button-floating-center",
665
+ "source": "@dereekb/dbx-web",
666
+ "module": "@dereekb/dbx-web",
667
+ "file": "src/lib/button/_button.scss",
668
+ "line": 265,
669
+ "declarations": [
670
+ {
671
+ "property": "margin-left",
672
+ "value": "auto"
673
+ },
674
+ {
675
+ "property": "margin-right",
676
+ "value": "auto"
677
+ }
678
+ ],
679
+ "role": "layout",
680
+ "intent": "horizontally center a `.dbx-button-floating` instead of right-aligning it",
681
+ "seeAlso": [
682
+ "button-floating",
683
+ "button-floating-top",
684
+ "button-floating-left"
685
+ ],
686
+ "parent": "button-floating"
687
+ },
688
+ {
689
+ "slug": "button-floating-fixed",
690
+ "selector": ".dbx-button-floating-fixed",
691
+ "source": "@dereekb/dbx-web",
692
+ "module": "@dereekb/dbx-web",
693
+ "file": "src/lib/button/_button.scss",
694
+ "line": 226,
695
+ "declarations": [
696
+ {
697
+ "property": "position",
698
+ "value": "fixed"
699
+ },
700
+ {
701
+ "property": "margin",
702
+ "value": "0"
703
+ },
704
+ {
705
+ "property": "right",
706
+ "value": "$button-floating-offset-x"
707
+ },
708
+ {
709
+ "property": "bottom",
710
+ "value": "$button-floating-offset-y"
711
+ }
712
+ ],
713
+ "role": "layout",
714
+ "intent": "pin a `.dbx-button-floating` to the viewport with `position: fixed` — a classic floating action button, `offset-x` / `offset-y` from the bottom-right corner by default (composes with `-top` / `-left` / `-center`). Breaks when an ancestor sets `transform`, `filter` or `contain`, which re-anchors fixed positioning to that ancestor",
715
+ "seeAlso": [
716
+ "button-floating",
717
+ "button-floating-top",
718
+ "button-floating-left",
719
+ "button-floating-center"
720
+ ],
721
+ "parent": "button-floating"
722
+ },
723
+ {
724
+ "slug": "button-floating-left",
725
+ "selector": ".dbx-button-floating-left",
726
+ "source": "@dereekb/dbx-web",
727
+ "module": "@dereekb/dbx-web",
728
+ "file": "src/lib/button/_button.scss",
729
+ "line": 250,
730
+ "declarations": [
731
+ {
732
+ "property": "margin-left",
733
+ "value": "0"
734
+ },
735
+ {
736
+ "property": "margin-right",
737
+ "value": "auto"
738
+ }
739
+ ],
740
+ "role": "layout",
741
+ "intent": "align a `.dbx-button-floating` to the left edge instead of the right",
742
+ "seeAlso": [
743
+ "button-floating",
744
+ "button-floating-top",
745
+ "button-floating-center"
746
+ ],
747
+ "parent": "button-floating"
748
+ },
749
+ {
750
+ "slug": "button-floating-top",
751
+ "selector": ".dbx-button-floating-top",
752
+ "source": "@dereekb/dbx-web",
753
+ "module": "@dereekb/dbx-web",
754
+ "file": "src/lib/button/_button.scss",
755
+ "line": 240,
756
+ "declarations": [
757
+ {
758
+ "property": "top",
759
+ "value": "$button-floating-offset-y"
760
+ },
761
+ {
762
+ "property": "bottom",
763
+ "value": "auto"
764
+ }
765
+ ],
766
+ "role": "layout",
767
+ "intent": "pin a `.dbx-button-floating` to the top edge (`offset-y` from the top) instead of the bottom — in sticky mode place the host at the start of its section",
768
+ "seeAlso": [
769
+ "button-floating",
770
+ "button-floating-left",
771
+ "button-floating-center"
772
+ ],
773
+ "parent": "button-floating"
774
+ },
587
775
  {
588
776
  "slug": "button-form-field-height",
589
777
  "selector": ".dbx-button-form-field-height",
@@ -1511,7 +1699,7 @@
1511
1699
  }
1512
1700
  ],
1513
1701
  "role": "layout",
1514
- "intent": "recessed surface pit (\"negative elevation\") — a semi-transparent tonal wash of the active dbxColor over `--mat-sys-surface-dim` (the dimmest/greyest M3 surface, the opposite end of the dim/bright pair from `.dbx-content-elevate`) so the lighter surface behind shows through, with a `padding-4` inset, used to visually nest content (e.g. logs, JSON, quoted blocks); rounded by default, opt out with `.dbx-corners-none`",
1702
+ "intent": "recessed surface pit (\"negative elevation\") — a semi-transparent tonal wash of the active dbxColor over `--mat-sys-surface-dim` (the dimmest/greyest M3 surface, the opposite end of the dim/bright pair from `.dbx-content-elevate`) so the lighter surface behind shows through, with a `padding-4` inset, used to visually nest content (e.g. logs, JSON, quoted blocks); rounded by default, opt out with `.dbx-corners-none`. Draws a thin 1px edge derived from its own background (a translucent ink of the active dbxColor + `on-surface` laid over the fill), so the edge follows dbxColor and background overrides — tune it with `--dbx-content-pit-border-width` (`0` removes it), `--dbx-content-pit-border-tone` (percentage), or `--dbx-content-pit-border-color`",
1515
1703
  "seeAlso": [
1516
1704
  "dbx-content-pit-scrollable",
1517
1705
  "dbx-content-border",
@@ -430,6 +430,58 @@
430
430
  },
431
431
  "componentScope": "button"
432
432
  },
433
+ {
434
+ "cssVariable": "--dbx-button-floating-offset",
435
+ "source": "dbx-web",
436
+ "role": "misc",
437
+ "intents": [
438
+ "button floating offset"
439
+ ],
440
+ "description": "Component-scoped override point (`button`) read in `packages/dbx-web/src/lib/button/_button.scss` — set it (from an app, or via the owning dbx directive where one exists) to override; unset, it falls back to `var(--dbx-padding-4)`.",
441
+ "defaults": {
442
+ "light": "var(--dbx-padding-4)"
443
+ },
444
+ "componentScope": "button"
445
+ },
446
+ {
447
+ "cssVariable": "--dbx-button-floating-offset-x",
448
+ "source": "dbx-web",
449
+ "role": "misc",
450
+ "intents": [
451
+ "button floating offset x"
452
+ ],
453
+ "description": "Component-scoped override point (`button`) read in `packages/dbx-web/src/lib/button/_button.scss` — set it (from an app, or via the owning dbx directive where one exists) to override; unset, it falls back to `var(--dbx-button-floating-offset, var(--dbx-padding-4))`.",
454
+ "defaults": {
455
+ "light": "var(--dbx-button-floating-offset, var(--dbx-padding-4))"
456
+ },
457
+ "componentScope": "button"
458
+ },
459
+ {
460
+ "cssVariable": "--dbx-button-floating-offset-y",
461
+ "source": "dbx-web",
462
+ "role": "misc",
463
+ "intents": [
464
+ "button floating offset y"
465
+ ],
466
+ "description": "Component-scoped override point (`button`) read in `packages/dbx-web/src/lib/button/_button.scss` — set it (from an app, or via the owning dbx directive where one exists) to override; unset, it falls back to `var(--dbx-button-floating-offset, var(--dbx-padding-4))`.",
467
+ "defaults": {
468
+ "light": "var(--dbx-button-floating-offset, var(--dbx-padding-4))"
469
+ },
470
+ "componentScope": "button"
471
+ },
472
+ {
473
+ "cssVariable": "--dbx-button-floating-z-index",
474
+ "source": "dbx-web",
475
+ "role": "misc",
476
+ "intents": [
477
+ "button floating z index"
478
+ ],
479
+ "description": "Component-scoped override point (`button`) read in `packages/dbx-web/src/lib/button/_button.scss` — set it (from an app, or via the owning dbx directive where one exists) to override; unset, it falls back to `10`.",
480
+ "defaults": {
481
+ "light": "10"
482
+ },
483
+ "componentScope": "button"
484
+ },
433
485
  {
434
486
  "cssVariable": "--dbx-button-icon-color",
435
487
  "source": "dbx-web",
@@ -991,6 +1043,45 @@
991
1043
  },
992
1044
  "componentScope": "content"
993
1045
  },
1046
+ {
1047
+ "cssVariable": "--dbx-content-pit-border-color",
1048
+ "source": "dbx-web",
1049
+ "role": "color",
1050
+ "intents": [
1051
+ "content pit border color"
1052
+ ],
1053
+ "description": "Component-scoped override point (`content`) read in `packages/dbx-web/src/lib/layout/content/_content.scss` — set it (from an app, or via the owning dbx directive where one exists) to override; unset, it falls back to `color-mix(in srgb, color-mix(in srgb, #{theming.$dbx-bg-color} 50%, var(--mat-sys-on-surface)) var(--dbx-content-pit-border-tone, 8%), transparent)`.",
1054
+ "defaults": {
1055
+ "light": "color-mix(in srgb, color-mix(in srgb, #{theming.$dbx-bg-color} 50%, var(--mat-sys-on-surface)) var(--dbx-content-pit-border-tone, 8%), transparent)"
1056
+ },
1057
+ "componentScope": "content"
1058
+ },
1059
+ {
1060
+ "cssVariable": "--dbx-content-pit-border-tone",
1061
+ "source": "dbx-web",
1062
+ "role": "misc",
1063
+ "intents": [
1064
+ "content pit border tone"
1065
+ ],
1066
+ "description": "Component-scoped override point (`content`) read in `packages/dbx-web/src/lib/layout/content/_content.scss` — set it (from an app, or via the owning dbx directive where one exists) to override; unset, it falls back to `8%`.",
1067
+ "defaults": {
1068
+ "light": "8%"
1069
+ },
1070
+ "componentScope": "content"
1071
+ },
1072
+ {
1073
+ "cssVariable": "--dbx-content-pit-border-width",
1074
+ "source": "dbx-web",
1075
+ "role": "size",
1076
+ "intents": [
1077
+ "content pit border width"
1078
+ ],
1079
+ "description": "Component-scoped override point (`content`) read in `packages/dbx-web/src/lib/layout/content/_content.scss` — set it (from an app, or via the owning dbx directive where one exists) to override; unset, it falls back to `1px`.",
1080
+ "defaults": {
1081
+ "light": "1px"
1082
+ },
1083
+ "componentScope": "content"
1084
+ },
994
1085
  {
995
1086
  "cssVariable": "--dbx-content-pit-opacity",
996
1087
  "source": "dbx-web",
@@ -75,7 +75,7 @@
75
75
  "selector": "dbx-button",
76
76
  "className": "DbxButtonComponent",
77
77
  "module": "@dereekb/dbx-web",
78
- "description": "Feature-rich button component that supports loading indicators, multiple Material button styles,\ncustom colors, icons, and floating action button (FAB) mode. Integrates with the dbxAction system\nfor automatic working/disabled state management.",
78
+ "description": "Feature-rich button component that supports loading indicators, multiple Material button styles,\ncustom colors, icons, and floating action button (FAB / extended FAB) mode. Integrates with the dbxAction system\nfor automatic working/disabled state management.",
79
79
  "inputs": [
80
80
  {
81
81
  "name": "bar",
@@ -93,13 +93,13 @@
93
93
  {
94
94
  "name": "buttonStyle",
95
95
  "type": "Maybe<DbxButtonStyle>",
96
- "description": "",
96
+ "description": "Style of the button. Individual style inputs (e.g. {@link color}, {@link raised}) take precedence.\n\nDeclared as a `model` so a directive composed onto the button (e.g. `dbxRotatingButton`) can set it.",
97
97
  "required": false
98
98
  },
99
99
  {
100
100
  "name": "color",
101
101
  "type": "Maybe<ThemePalette | DbxColorInput>",
102
- "description": "",
102
+ "description": "Color of the button. Takes precedence over the {@link buttonStyle} color.\n\nDeclared as a `model` so a directive composed onto the button (e.g. `dbxRotatingButton`) can set it.",
103
103
  "required": false
104
104
  },
105
105
  {
@@ -165,7 +165,7 @@
165
165
  {
166
166
  "name": "fab",
167
167
  "type": "boolean",
168
- "description": "",
168
+ "description": "Renders a Material FAB, or an extended FAB when the button has text. Combine with {@link iconOnly} for a round icon button.",
169
169
  "required": false,
170
170
  "default": "false"
171
171
  },
@@ -203,6 +203,52 @@
203
203
  "example": "<dbx-button [text]=\"'Save'\" raised [dbxAction]=\"saveAction\">\n</dbx-button>",
204
204
  "minimalExample": "<dbx-button text=\"Save\" raised [dbxAction]=\"saveAction\"></dbx-button>"
205
205
  },
206
+ {
207
+ "slug": "button-floating",
208
+ "category": "button",
209
+ "kind": "directive",
210
+ "selector": "[dbxButtonFloating]",
211
+ "className": "DbxButtonFloatingDirective",
212
+ "module": "@dereekb/dbx-web",
213
+ "description": "Floats its host — typically a `dbx-button` with `dbxActionButton`, or a wrapper holding several buttons — over the content\nby applying the `.dbx-button-floating` utility classes.\n\nIn the default `'sticky'` mode the host stays pinned to the edge of its scroll container while its parent section is on\nscreen, which suits a Save button at the end of a long form. Place the host at the end of its section for bottom positions and\nat the start for top positions. Sticky stops working when an ancestor between the host and the scroll container sets\n`overflow`. In `'fixed'` mode the host is pinned to the viewport; fixed positioning is re-anchored to any ancestor that sets\n`transform`, `filter` or `contain`.\n\nSet `fab` on the `dbx-button` for the Material FAB presentation (an extended FAB when it has text). Pair with\n`dbxActionEnforceModified` on the action to keep a floating Save button disabled until the form has changes.",
214
+ "inputs": [
215
+ {
216
+ "name": "dbxButtonFloating",
217
+ "type": "Maybe<DbxButtonFloatingConfig | DbxButtonFloatingPosition | ''>",
218
+ "description": "Config for the floating button, or a {@link DbxButtonFloatingPosition} shorthand. Empty when used as a bare attribute.",
219
+ "required": false
220
+ },
221
+ {
222
+ "name": "dbxButtonFloatingMode",
223
+ "type": "Maybe<DbxButtonFloatingMode>",
224
+ "description": "",
225
+ "required": false
226
+ },
227
+ {
228
+ "name": "dbxButtonFloatingPosition",
229
+ "type": "Maybe<DbxButtonFloatingPosition>",
230
+ "description": "",
231
+ "required": false
232
+ },
233
+ {
234
+ "name": "dbxButtonFloatingOffset",
235
+ "type": "Maybe<DbxButtonFloatingOffset>",
236
+ "description": "",
237
+ "required": false
238
+ }
239
+ ],
240
+ "outputs": [],
241
+ "relatedSlugs": [
242
+ "button",
243
+ "button-spacer",
244
+ "content-pit"
245
+ ],
246
+ "skillRefs": [
247
+ "dbx__ref__dbx-ui-building-blocks"
248
+ ],
249
+ "example": "<div dbxAction dbxActionEnforceModified [dbxActionHandler]=\"handleSave\">\n <my-form dbxActionForm></my-form>\n <dbx-button dbxButtonFloating fab color=\"primary\" icon=\"save\" text=\"Save\" dbxActionButton></dbx-button>\n</div>",
250
+ "minimalExample": "<dbx-button dbxButtonFloating fab color=\"primary\" icon=\"save\" text=\"Save\" dbxActionButton></dbx-button>"
251
+ },
206
252
  {
207
253
  "slug": "button-spacer",
208
254
  "category": "button",
@@ -757,7 +803,7 @@
757
803
  "selector": "dbx-progress-spinner-button,dbx-spinner-button",
758
804
  "className": "DbxProgressSpinnerButtonComponent",
759
805
  "module": "@dereekb/dbx-web",
760
- "description": "Progress button that overlays a Material progress spinner on the button while working.\nSupports icon-only, FAB, and text button modes with automatic spinner sizing.",
806
+ "description": "Progress button that overlays a Material progress spinner on the button while working.\nSupports icon-only, FAB, extended FAB, and text button modes with automatic spinner sizing.\n\nA `fab` config renders a Material `mat-fab` — an extended FAB when the button has text — while `fab` + `iconOnly` renders a\nround icon button.",
761
807
  "inputs": [
762
808
  {
763
809
  "name": "config",
@@ -808,6 +854,36 @@
808
854
  "example": "<dbx-progress-spinner-button [config]=\"cfg\" (btnClick)=\"onClick()\"></dbx-progress-spinner-button>",
809
855
  "minimalExample": "<dbx-progress-spinner-button [config]=\"cfg\"></dbx-progress-spinner-button>"
810
856
  },
857
+ {
858
+ "slug": "rotating-button",
859
+ "category": "button",
860
+ "kind": "directive",
861
+ "selector": "[dbxRotatingButton]",
862
+ "className": "DbxRotatingButtonDirective",
863
+ "module": "@dereekb/dbx-web",
864
+ "description": "Turns a `dbx-button` into a rotating / multi-state button. Each click moves to the next configured state, and the\nstate's icon, text, style and aria-label are set on the button.\n\nUse {@link dbxTristateRotatingButtonConfig} for a three-state / 3-phase (default/on/off) toggle. The directive sets the\nbutton's `icon`, `text`, `buttonStyle` and `ariaLabel`, so configure those through the config rather than on the button.\n\nThe button stops click propagation by default, so a rotating button works inside clickable list rows.",
865
+ "inputs": [
866
+ {
867
+ "name": "dbxRotatingButton",
868
+ "type": "Maybe<DbxRotatingButtonConfig<T>>",
869
+ "description": "",
870
+ "required": false
871
+ },
872
+ {
873
+ "name": "dbxRotatingButtonValue",
874
+ "type": "Maybe<T>",
875
+ "description": "The current value. Two-way bindable with `[(dbxRotatingButtonValue)]`.",
876
+ "required": false
877
+ }
878
+ ],
879
+ "outputs": [],
880
+ "relatedSlugs": [
881
+ "button",
882
+ "icon-button"
883
+ ],
884
+ "example": "readonly tristateConfig = dbxTristateRotatingButtonConfig({ label: 'Text', defaultValue: false });",
885
+ "minimalExample": "<dbx-button iconOnly [dbxRotatingButton]=\"tristateConfig\" [(dbxRotatingButtonValue)]=\"emailEnabled\"></dbx-button>"
886
+ },
811
887
  {
812
888
  "slug": "browser-agent-view",
813
889
  "category": "misc",
@@ -828,6 +904,12 @@
828
904
  "type": "Maybe<boolean>",
829
905
  "description": "",
830
906
  "required": false
907
+ },
908
+ {
909
+ "name": "overrideKey",
910
+ "type": "Maybe<DbxBrowserAgentViewEntryKey>",
911
+ "description": "Key of the manually selected entry. Can be bound two-way to persist the user's choice.",
912
+ "required": false
831
913
  }
832
914
  ],
833
915
  "outputs": [],
@@ -987,7 +1069,7 @@
987
1069
  "selector": "dbx-prompt-confirm",
988
1070
  "className": "DbxPromptConfirmComponent",
989
1071
  "module": "@dereekb/dbx-web",
990
- "description": "Renders a confirmation prompt with customizable title, message, confirm, and cancel buttons.",
1072
+ "description": "Renders a confirmation prompt with customizable title, message, confirm, and cancel buttons.\n\nProjected content shows above the buttons. Content marked with the `buttons` attribute shows in the same row, before the\nconfirm button.",
991
1073
  "inputs": [
992
1074
  {
993
1075
  "name": "config",
@@ -1009,7 +1091,7 @@
1009
1091
  "description": ""
1010
1092
  }
1011
1093
  ],
1012
- "contentProjection": "<ng-content></ng-content>",
1094
+ "contentProjection": "<ng-content></ng-content>; <ng-content select=\"[buttons]\"></ng-content>",
1013
1095
  "relatedSlugs": [
1014
1096
  "prompt",
1015
1097
  "action-confirm"
@@ -2075,7 +2157,20 @@
2075
2157
  "className": "DbxColorDirective",
2076
2158
  "module": "@dereekb/dbx-web",
2077
2159
  "description": "Provides themed color *tokens* to the host element — it never paints a background itself.\n\nAccepts either a {@link DbxThemeColor} string (e.g. `'primary'`, `'success'`) or a {@link DbxColorConfig}\nobject carrying arbitrary CSS color values (any hex, `rgb()`, `hsl()`, `var(...)`, `color-mix(...)`, etc.).\nThe directive sets the `--dbx-bg-color-current` / `--dbx-color-current` (and tone) custom properties and adds the\n`.dbx-color` marker class, leaving the actual painting to either the explicit `.dbx-color-bg` utility on the same\nelement or a colored-surface component's own `.dbx-color`-scoped SCSS (e.g. `<dbx-bar>`, `<dbx-button>`, `<dbx-loading>`,\n`<dbx-icon-tile>`, `<mat-card>`) that reads the inherited tokens.\n\nOptionally set {@link dbxColorTone} (0-100) to control background opacity for a tonal/muted appearance.\nWhen tonal mode is active, the `dbx-color-tonal` CSS class is added and a CSS rule overrides the text\ncolor to the vibrant color (via `--dbx-bg-color-current`).\n\nThe standalone `[dbxColorTone]` input wins over `config.tone` when both are set; the same precedence\napplies to tonal mode.",
2078
- "inputs": [],
2160
+ "inputs": [
2161
+ {
2162
+ "name": "dbxColor",
2163
+ "type": "Maybe<DbxColorInput>",
2164
+ "description": "The color to provide tokens for. Declared as a `model` so a colored-surface component on the same host (e.g.\n`dbx-button`) can push its resolved color into the directive, keeping it the single token provider on the element.",
2165
+ "required": false
2166
+ },
2167
+ {
2168
+ "name": "dbxColorTone",
2169
+ "type": "Maybe<DbxColorTone>",
2170
+ "description": "Background tone level (0-100). When set, the background becomes semi-transparent\nand text color switches to the vibrant theme color for a tonal appearance.\n\nDeclared as a `model` for the same reason as {@link dbxColor}: a colored-surface component on the same host (e.g.\n`dbx-avatar-view`) resolves its tone from its own inputs/context and pushes it here, keeping this directive the\nsingle token provider on the element.",
2171
+ "required": false
2172
+ }
2173
+ ],
2079
2174
  "outputs": [],
2080
2175
  "relatedSlugs": [
2081
2176
  "text-color",
@@ -513,6 +513,125 @@
513
513
  "derivedComposites": [],
514
514
  "derivedFieldOverrides": []
515
515
  },
516
+ {
517
+ "slug": "notification-users-with-text-phone-number-query",
518
+ "name": "notificationUsersWithTextPhoneNumberQuery",
519
+ "module": "@dereekb/firebase",
520
+ "subpath": "model/notification/notification.query",
521
+ "signature": "notificationUsersWithTextPhoneNumberQuery(phoneNumber: E164PhoneNumber): FirestoreQueryConstraint[]",
522
+ "description": "Query constraints for finding {@link NotificationUser} documents whose texting number (`gc.t`) is the given phone number.\n\nUsed by the text opt-out sync to find the users a STOP reply came from.",
523
+ "model": "NotificationUser",
524
+ "collection": "nu",
525
+ "isNested": false,
526
+ "scope": "COLLECTION",
527
+ "manual": false,
528
+ "skip": false,
529
+ "category": "lookup",
530
+ "params": [
531
+ {
532
+ "name": "phoneNumber",
533
+ "type": "E164PhoneNumber",
534
+ "description": "- The texting number to match.",
535
+ "optional": false
536
+ }
537
+ ],
538
+ "returns": "Array of Firestore query constraints filtering for users with that texting number.",
539
+ "tags": [
540
+ "lookup",
541
+ "notificationuser",
542
+ "notification",
543
+ "users",
544
+ "with",
545
+ "text",
546
+ "phone",
547
+ "number",
548
+ "query",
549
+ "notificationuserswithtextphonenumberquery",
550
+ "user",
551
+ "constraints",
552
+ "finding",
553
+ "link",
554
+ "documents",
555
+ "whose",
556
+ "texting",
557
+ "given",
558
+ "used"
559
+ ],
560
+ "constraintSequences": [
561
+ {
562
+ "pathLabel": "all",
563
+ "entries": [
564
+ {
565
+ "kind": "where",
566
+ "fieldPath": "gc.t",
567
+ "operator": "=="
568
+ }
569
+ ]
570
+ }
571
+ ],
572
+ "derivedComposites": [],
573
+ "derivedFieldOverrides": []
574
+ },
575
+ {
576
+ "slug": "notification-users-with-stopped-text-phone-number-query",
577
+ "name": "notificationUsersWithStoppedTextPhoneNumberQuery",
578
+ "module": "@dereekb/firebase",
579
+ "subpath": "model/notification/notification.query",
580
+ "signature": "notificationUsersWithStoppedTextPhoneNumberQuery(phoneNumber: E164PhoneNumber): FirestoreQueryConstraint[]",
581
+ "description": "Query constraints for finding {@link NotificationUser} documents that have the given phone number in their stopped numbers (`tso`).\n\nUsed by the text opt-out sync to find the users a START reply turns texts back on for.",
582
+ "model": "NotificationUser",
583
+ "collection": "nu",
584
+ "isNested": false,
585
+ "scope": "COLLECTION",
586
+ "manual": false,
587
+ "skip": false,
588
+ "category": "lookup",
589
+ "params": [
590
+ {
591
+ "name": "phoneNumber",
592
+ "type": "E164PhoneNumber",
593
+ "description": "- The stopped phone number to match.",
594
+ "optional": false
595
+ }
596
+ ],
597
+ "returns": "Array of Firestore query constraints filtering for users that stopped that number.",
598
+ "tags": [
599
+ "lookup",
600
+ "notificationuser",
601
+ "notification",
602
+ "users",
603
+ "with",
604
+ "stopped",
605
+ "text",
606
+ "phone",
607
+ "number",
608
+ "query",
609
+ "notificationuserswithstoppedtextphonenumberquery",
610
+ "user",
611
+ "constraints",
612
+ "finding",
613
+ "link",
614
+ "documents",
615
+ "have",
616
+ "given",
617
+ "their",
618
+ "numbers"
619
+ ],
620
+ "constraintSequences": [
621
+ {
622
+ "pathLabel": "all",
623
+ "entries": [
624
+ {
625
+ "kind": "where",
626
+ "fieldPath": "tso",
627
+ "operator": "array-contains"
628
+ }
629
+ ]
630
+ }
631
+ ],
632
+ "derivedComposites": [],
633
+ "derivedFieldOverrides": []
634
+ },
516
635
  {
517
636
  "slug": "notification-summaries-flagged-for-needs-initialization-query",
518
637
  "name": "notificationSummariesFlaggedForNeedsInitializationQuery",
@@ -298,6 +298,20 @@
298
298
  ],
299
299
  "notes": "Names one upload \"slot\" within a FormSpace — the resume, the cover letter, the photo of the ID.\n\nA slot is stored on the uploaded {@link StorageFile} as its `pg` (purposeSubgroup), which is what makes\n\"replace the file in this slot\" the existing `flagPreviousForDelete` behaviour rather than new machinery."
300
300
  },
301
+ {
302
+ "name": "NotificationTemplateTypeInfoGroupKey",
303
+ "package": "@dereekb/firebase",
304
+ "module": "src/lib/model/notification/notification.details",
305
+ "kind": "semantic-type",
306
+ "definition": "string",
307
+ "baseType": "string",
308
+ "topics": [
309
+ "identifier",
310
+ "string",
311
+ "dereekb-firebase:notification"
312
+ ],
313
+ "notes": "Key of a {@link NotificationTemplateTypeInfoGroup}. Template types whose groups share a key are shown together."
314
+ },
301
315
  {
302
316
  "name": "NotificationTemplateType",
303
317
  "package": "@dereekb/firebase",
@@ -12369,6 +12369,44 @@
12369
12369
  "read-website-protocol"
12370
12370
  ]
12371
12371
  },
12372
+ {
12373
+ "slug": "website-url-relative-path-functions",
12374
+ "name": "websiteUrlRelativePathFunctions",
12375
+ "kind": "factory",
12376
+ "category": "string",
12377
+ "module": "@dereekb/util",
12378
+ "subpath": "string/url",
12379
+ "signature": "websiteUrlRelativePathFunctions(config: WebsiteUrlRelativePathFunctionsConfig): WebsiteUrlRelativePathFunctions",
12380
+ "description": "Creates {@link WebsiteUrlRelativePathFunctions} for the configured base url.\n\nInputs may be the relative path (`dereekb`), a url without a protocol (`linkedin.com/in/dereekb`), or a full url (`https://www.linkedin.com/in/dereekb/?trk=abc`).\nDomains are compared case-insensitively and a leading `www.` is ignored. Query parameters and trailing slashes are removed from the relative path.",
12381
+ "params": [
12382
+ {
12383
+ "name": "config",
12384
+ "type": "WebsiteUrlRelativePathFunctionsConfig",
12385
+ "description": "- The base url and protocol configuration.",
12386
+ "optional": false
12387
+ }
12388
+ ],
12389
+ "returns": "Functions for reading relative paths and building full website urls.",
12390
+ "tags": [
12391
+ "string",
12392
+ "url",
12393
+ "path",
12394
+ "relative",
12395
+ "base",
12396
+ "slug",
12397
+ "username",
12398
+ "profile",
12399
+ "factory",
12400
+ "website",
12401
+ "functions",
12402
+ "websiteurlrelativepathfunctions"
12403
+ ],
12404
+ "example": "const linkedIn = websiteUrlRelativePathFunctions({ baseUrl: 'https://linkedin.com/in/' });\n\nlinkedIn.toRelativePath('linkedin.com/in/dereekb'); // 'dereekb'\nlinkedIn.toWebsiteUrl('dereekb'); // 'https://linkedin.com/in/dereekb'\nlinkedIn.toWebsiteUrl('https://twitter.com/dereekb'); // undefined",
12405
+ "relatedSlugs": [
12406
+ "isolate-website-path-function",
12407
+ "website-url-details"
12408
+ ]
12409
+ },
12372
12410
  {
12373
12411
  "slug": "expand-flatten-tree-function",
12374
12412
  "name": "expandFlattenTreeFunction",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dereekb/dbx-components-mcp",
3
- "version": "14.13.0",
3
+ "version": "14.15.0",
4
4
  "sideEffects": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,7 @@
12
12
  "ts-morph": "^21.0.0"
13
13
  },
14
14
  "peerDependencies": {
15
- "@dereekb/dbx-cli": "14.13.0",
16
- "@dereekb/util": "14.13.0"
15
+ "@dereekb/dbx-cli": "14.15.0",
16
+ "@dereekb/util": "14.15.0"
17
17
  }
18
18
  }