feat: complete SSR CRM and refine auth UI
This commit is contained in:
@@ -93,6 +93,25 @@
|
|||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"examples/crm-app": {
|
||||||
|
"name": "wrnexus-crm-example",
|
||||||
|
"version": "0.8.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@wrnexus/auth": "workspace:*",
|
||||||
|
"@wrnexus/authz": "workspace:*",
|
||||||
|
"@wrnexus/core": "workspace:*",
|
||||||
|
"@wrnexus/db": "workspace:*",
|
||||||
|
"@wrnexus/styles": "workspace:*",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@iconify-json/lucide": "^1.2.123",
|
||||||
|
"@iconify/tailwind4": "^1.2.3",
|
||||||
|
"@tailwindcss/cli": "^4.3.3",
|
||||||
|
"@types/bun": "^1.3.14",
|
||||||
|
"tailwindcss": "^4.3.3",
|
||||||
|
"typescript": "^6.0.3",
|
||||||
|
},
|
||||||
|
},
|
||||||
"examples/i18n-showcase": {
|
"examples/i18n-showcase": {
|
||||||
"name": "i18n-showcase",
|
"name": "i18n-showcase",
|
||||||
"version": "0.8.0",
|
"version": "0.8.0",
|
||||||
@@ -216,7 +235,7 @@
|
|||||||
},
|
},
|
||||||
"packages/auth": {
|
"packages/auth": {
|
||||||
"name": "@wrnexus/auth",
|
"name": "@wrnexus/auth",
|
||||||
"version": "0.8.12",
|
"version": "0.8.13",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/authz": "workspace:*",
|
"@wrnexus/authz": "workspace:*",
|
||||||
"@wrnexus/captcha": "workspace:*",
|
"@wrnexus/captcha": "workspace:*",
|
||||||
@@ -272,7 +291,7 @@
|
|||||||
},
|
},
|
||||||
"packages/cli": {
|
"packages/cli": {
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.8.46",
|
"version": "0.8.47",
|
||||||
"bin": {
|
"bin": {
|
||||||
"wrnexus": "src/index.ts",
|
"wrnexus": "src/index.ts",
|
||||||
},
|
},
|
||||||
@@ -337,7 +356,7 @@
|
|||||||
},
|
},
|
||||||
"packages/dev-server": {
|
"packages/dev-server": {
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.8.41",
|
"version": "0.8.42",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/authz": "workspace:*",
|
"@wrnexus/authz": "workspace:*",
|
||||||
"@wrnexus/cache": "workspace:*",
|
"@wrnexus/cache": "workspace:*",
|
||||||
@@ -642,7 +661,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.8.20",
|
"version": "0.8.21",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
@@ -1406,6 +1425,8 @@
|
|||||||
|
|
||||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||||
|
|
||||||
|
"wrnexus-crm-example": ["wrnexus-crm-example@workspace:examples/crm-app"],
|
||||||
|
|
||||||
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
|
"ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="],
|
||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|||||||
@@ -1840,7 +1840,7 @@
|
|||||||
"Position",
|
"Position",
|
||||||
"Range",
|
"Range",
|
||||||
"TextDocument",
|
"TextDocument",
|
||||||
"WRN_COMPLETIONS",
|
"WRN_KEYWORDS",
|
||||||
"WorkspaceCompletionItem",
|
"WorkspaceCompletionItem",
|
||||||
"clearWorkspaceIndexCache",
|
"clearWorkspaceIndexCache",
|
||||||
"completionItems",
|
"completionItems",
|
||||||
@@ -1861,7 +1861,11 @@
|
|||||||
"workspaceCompletionItems",
|
"workspaceCompletionItems",
|
||||||
"workspaceSymbolLocations"
|
"workspaceSymbolLocations"
|
||||||
],
|
],
|
||||||
"./server": []
|
"./server": [
|
||||||
|
"ApiCallCompletionItem",
|
||||||
|
"apiCallCompletions",
|
||||||
|
"apiCallHover"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"@wrnexus/mcp": {
|
"@wrnexus/mcp": {
|
||||||
".": [
|
".": [
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// WRN editor language server source hash: f75d793688bdb42225d2314660c87852a108b76311974be9307d11e42ecfbb41
|
// WRN editor language server source hash: 70d2454817cc3aa546304c880e0a82313bb73b50051556bf7febd12e6a46c78c
|
||||||
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
|
// WRN editor language server generator hash: f593a44aaf05495b789ce7a3086bee1eebb951b884d41c0e017bbcfe5f547e72
|
||||||
// @bun @bun-cjs
|
// @bun @bun-cjs
|
||||||
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
|
(function(exports, require, module, __filename, __dirname) {var __create = Object.create;
|
||||||
var __getProtoOf = Object.getPrototypeOf;
|
var __getProtoOf = Object.getPrototypeOf;
|
||||||
var __defProp = Object.defineProperty;
|
var __defProp = Object.defineProperty;
|
||||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
function __accessProp(key) {
|
function __accessProp(key) {
|
||||||
return this[key];
|
return this[key];
|
||||||
@@ -32,7 +33,37 @@ var __toESM = (mod, isNodeMode, target) => {
|
|||||||
cache.set(mod, to);
|
cache.set(mod, to);
|
||||||
return to;
|
return to;
|
||||||
};
|
};
|
||||||
|
var __toCommonJS = (from) => {
|
||||||
|
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
||||||
|
if (entry)
|
||||||
|
return entry;
|
||||||
|
entry = __defProp({}, "__esModule", { value: true });
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (var key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(entry, key))
|
||||||
|
__defProp(entry, key, {
|
||||||
|
get: __accessProp.bind(from, key),
|
||||||
|
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
||||||
|
});
|
||||||
|
}
|
||||||
|
__moduleCache.set(from, entry);
|
||||||
|
return entry;
|
||||||
|
};
|
||||||
|
var __moduleCache;
|
||||||
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
||||||
|
var __returnValue = (v) => v;
|
||||||
|
function __exportSetter(name, newValue) {
|
||||||
|
this[name] = __returnValue.bind(null, newValue);
|
||||||
|
}
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, {
|
||||||
|
get: all[name],
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
set: __exportSetter.bind(all, name)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/typescript.js
|
// node_modules/.bun/typescript@6.0.3/node_modules/typescript/lib/typescript.js
|
||||||
var require_typescript = __commonJS((exports2, module2) => {
|
var require_typescript = __commonJS((exports2, module2) => {
|
||||||
@@ -54,10 +85,10 @@ var require_typescript = __commonJS((exports2, module2) => {
|
|||||||
var ts = {};
|
var ts = {};
|
||||||
((module3) => {
|
((module3) => {
|
||||||
var __defProp2 = Object.defineProperty;
|
var __defProp2 = Object.defineProperty;
|
||||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
|
||||||
var __getOwnPropNames2 = Object.getOwnPropertyNames;
|
var __getOwnPropNames2 = Object.getOwnPropertyNames;
|
||||||
var __hasOwnProp2 = Object.prototype.hasOwnProperty;
|
var __hasOwnProp2 = Object.prototype.hasOwnProperty;
|
||||||
var __export = (target, all) => {
|
var __export2 = (target, all) => {
|
||||||
for (var name in all)
|
for (var name in all)
|
||||||
__defProp2(target, name, { get: all[name], enumerable: true });
|
__defProp2(target, name, { get: all[name], enumerable: true });
|
||||||
};
|
};
|
||||||
@@ -65,13 +96,13 @@ var require_typescript = __commonJS((exports2, module2) => {
|
|||||||
if (from && typeof from === "object" || typeof from === "function") {
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
for (let key of __getOwnPropNames2(from))
|
for (let key of __getOwnPropNames2(from))
|
||||||
if (!__hasOwnProp2.call(to, key) && key !== except)
|
if (!__hasOwnProp2.call(to, key) && key !== except)
|
||||||
__defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
__defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
|
||||||
}
|
}
|
||||||
return to;
|
return to;
|
||||||
};
|
};
|
||||||
var __toCommonJS = (mod) => (__copyProps, mod);
|
var __toCommonJS2 = (mod) => (__copyProps, mod);
|
||||||
var typescript_exports = {};
|
var typescript_exports = {};
|
||||||
__export(typescript_exports, {
|
__export2(typescript_exports, {
|
||||||
ANONYMOUS: () => ANONYMOUS,
|
ANONYMOUS: () => ANONYMOUS,
|
||||||
AccessFlags: () => AccessFlags,
|
AccessFlags: () => AccessFlags,
|
||||||
AssertionLevel: () => AssertionLevel,
|
AssertionLevel: () => AssertionLevel,
|
||||||
@@ -2321,7 +2352,7 @@ var require_typescript = __commonJS((exports2, module2) => {
|
|||||||
writeFileEnsuringDirectories: () => writeFileEnsuringDirectories,
|
writeFileEnsuringDirectories: () => writeFileEnsuringDirectories,
|
||||||
zipWith: () => zipWith
|
zipWith: () => zipWith
|
||||||
});
|
});
|
||||||
module3.exports = __toCommonJS(typescript_exports);
|
module3.exports = __toCommonJS2(typescript_exports);
|
||||||
var versionMajorMinor = "6.0";
|
var versionMajorMinor = "6.0";
|
||||||
var version = "6.0.3";
|
var version = "6.0.3";
|
||||||
var Comparison = /* @__PURE__ */ ((Comparison3) => {
|
var Comparison = /* @__PURE__ */ ((Comparison3) => {
|
||||||
@@ -4851,8 +4882,8 @@ ${lanes.join(`
|
|||||||
return compareValues(left.length, right.length);
|
return compareValues(left.length, right.length);
|
||||||
}
|
}
|
||||||
var VersionRange = class _VersionRange {
|
var VersionRange = class _VersionRange {
|
||||||
constructor(spec2) {
|
constructor(spec3) {
|
||||||
this._alternatives = spec2 ? Debug.checkDefined(parseRange(spec2), "Invalid range spec.") : emptyArray;
|
this._alternatives = spec3 ? Debug.checkDefined(parseRange(spec3), "Invalid range spec.") : emptyArray;
|
||||||
}
|
}
|
||||||
static tryParse(text) {
|
static tryParse(text) {
|
||||||
const sets = parseRange(text);
|
const sets = parseRange(text);
|
||||||
@@ -5055,7 +5086,7 @@ ${lanes.join(`
|
|||||||
}
|
}
|
||||||
var timestamp = nativePerformanceTime ? () => nativePerformanceTime.now() : Date.now;
|
var timestamp = nativePerformanceTime ? () => nativePerformanceTime.now() : Date.now;
|
||||||
var ts_performance_exports = {};
|
var ts_performance_exports = {};
|
||||||
__export(ts_performance_exports, {
|
__export2(ts_performance_exports, {
|
||||||
clearMarks: () => clearMarks,
|
clearMarks: () => clearMarks,
|
||||||
clearMeasures: () => clearMeasures,
|
clearMeasures: () => clearMeasures,
|
||||||
createTimer: () => createTimer,
|
createTimer: () => createTimer,
|
||||||
@@ -21581,19 +21612,19 @@ ${lanes.join(`
|
|||||||
if (specs === undefined || specs.length === 0) {
|
if (specs === undefined || specs.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return flatMap(specs, (spec2) => spec2 && getSubPatternFromSpec(spec2, basePath, usage, wildcardMatchers[usage]));
|
return flatMap(specs, (spec3) => spec3 && getSubPatternFromSpec(spec3, basePath, usage, wildcardMatchers[usage]));
|
||||||
}
|
}
|
||||||
function isImplicitGlob(lastPathComponent) {
|
function isImplicitGlob(lastPathComponent) {
|
||||||
return !/[.*?]/.test(lastPathComponent);
|
return !/[.*?]/.test(lastPathComponent);
|
||||||
}
|
}
|
||||||
function getPatternFromSpec(spec2, basePath, usage) {
|
function getPatternFromSpec(spec3, basePath, usage) {
|
||||||
const pattern = spec2 && getSubPatternFromSpec(spec2, basePath, usage, wildcardMatchers[usage]);
|
const pattern = spec3 && getSubPatternFromSpec(spec3, basePath, usage, wildcardMatchers[usage]);
|
||||||
return pattern && `^(?:${pattern})${usage === "exclude" ? "(?:$|/)" : "$"}`;
|
return pattern && `^(?:${pattern})${usage === "exclude" ? "(?:$|/)" : "$"}`;
|
||||||
}
|
}
|
||||||
function getSubPatternFromSpec(spec2, basePath, usage, { singleAsteriskRegexFragment, doubleAsteriskRegexFragment, replaceWildcardCharacter: replaceWildcardCharacter2 } = wildcardMatchers[usage]) {
|
function getSubPatternFromSpec(spec3, basePath, usage, { singleAsteriskRegexFragment, doubleAsteriskRegexFragment, replaceWildcardCharacter: replaceWildcardCharacter2 } = wildcardMatchers[usage]) {
|
||||||
let subpattern = "";
|
let subpattern = "";
|
||||||
let hasWrittenComponent = false;
|
let hasWrittenComponent = false;
|
||||||
const components = getNormalizedPathComponents(spec2, basePath);
|
const components = getNormalizedPathComponents(spec3, basePath);
|
||||||
const lastComponent = last(components);
|
const lastComponent = last(components);
|
||||||
if (usage !== "exclude" && lastComponent === "**") {
|
if (usage !== "exclude" && lastComponent === "**") {
|
||||||
return;
|
return;
|
||||||
@@ -40722,8 +40753,8 @@ ${lanes.join(`
|
|||||||
const wildcardFiles = arrayFrom(wildcardFileMap.values());
|
const wildcardFiles = arrayFrom(wildcardFileMap.values());
|
||||||
return literalFiles.concat(wildcardFiles, arrayFrom(wildCardJsonFileMap.values()));
|
return literalFiles.concat(wildcardFiles, arrayFrom(wildCardJsonFileMap.values()));
|
||||||
}
|
}
|
||||||
function isExcludedFile(pathToCheck, spec2, basePath, useCaseSensitiveFileNames2, currentDirectory) {
|
function isExcludedFile(pathToCheck, spec3, basePath, useCaseSensitiveFileNames2, currentDirectory) {
|
||||||
const { validatedFilesSpec, validatedIncludeSpecs, validatedExcludeSpecs } = spec2;
|
const { validatedFilesSpec, validatedIncludeSpecs, validatedExcludeSpecs } = spec3;
|
||||||
if (!length(validatedIncludeSpecs) || !length(validatedExcludeSpecs))
|
if (!length(validatedIncludeSpecs) || !length(validatedExcludeSpecs))
|
||||||
return false;
|
return false;
|
||||||
basePath = normalizePath(basePath);
|
basePath = normalizePath(basePath);
|
||||||
@@ -40745,7 +40776,7 @@ ${lanes.join(`
|
|||||||
return lastDotIndex > wildcardIndex;
|
return lastDotIndex > wildcardIndex;
|
||||||
}
|
}
|
||||||
function matchesExclude(pathToCheck, excludeSpecs, useCaseSensitiveFileNames2, currentDirectory) {
|
function matchesExclude(pathToCheck, excludeSpecs, useCaseSensitiveFileNames2, currentDirectory) {
|
||||||
return matchesExcludeWorker(pathToCheck, filter(excludeSpecs, (spec2) => !invalidDotDotAfterRecursiveWildcard(spec2)), useCaseSensitiveFileNames2, currentDirectory);
|
return matchesExcludeWorker(pathToCheck, filter(excludeSpecs, (spec3) => !invalidDotDotAfterRecursiveWildcard(spec3)), useCaseSensitiveFileNames2, currentDirectory);
|
||||||
}
|
}
|
||||||
function matchesExcludeWorker(pathToCheck, excludeSpecs, useCaseSensitiveFileNames2, currentDirectory, basePath) {
|
function matchesExcludeWorker(pathToCheck, excludeSpecs, useCaseSensitiveFileNames2, currentDirectory, basePath) {
|
||||||
const excludePattern = getRegularExpressionForWildcard(excludeSpecs, combinePaths(normalizePath(currentDirectory), basePath), "exclude");
|
const excludePattern = getRegularExpressionForWildcard(excludeSpecs, combinePaths(normalizePath(currentDirectory), basePath), "exclude");
|
||||||
@@ -40757,26 +40788,26 @@ ${lanes.join(`
|
|||||||
return !hasExtension(pathToCheck) && excludeRegex.test(ensureTrailingDirectorySeparator(pathToCheck));
|
return !hasExtension(pathToCheck) && excludeRegex.test(ensureTrailingDirectorySeparator(pathToCheck));
|
||||||
}
|
}
|
||||||
function validateSpecs(specs, errors, disallowTrailingRecursion, jsonSourceFile, specKey) {
|
function validateSpecs(specs, errors, disallowTrailingRecursion, jsonSourceFile, specKey) {
|
||||||
return specs.filter((spec2) => {
|
return specs.filter((spec3) => {
|
||||||
if (!isString(spec2))
|
if (!isString(spec3))
|
||||||
return false;
|
return false;
|
||||||
const diag2 = specToDiagnostic(spec2, disallowTrailingRecursion);
|
const diag2 = specToDiagnostic(spec3, disallowTrailingRecursion);
|
||||||
if (diag2 !== undefined) {
|
if (diag2 !== undefined) {
|
||||||
errors.push(createDiagnostic(...diag2));
|
errors.push(createDiagnostic(...diag2));
|
||||||
}
|
}
|
||||||
return diag2 === undefined;
|
return diag2 === undefined;
|
||||||
});
|
});
|
||||||
function createDiagnostic(message, spec2) {
|
function createDiagnostic(message, spec3) {
|
||||||
const element = getTsConfigPropArrayElementValue(jsonSourceFile, specKey, spec2);
|
const element = getTsConfigPropArrayElementValue(jsonSourceFile, specKey, spec3);
|
||||||
return createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(jsonSourceFile, element, message, spec2);
|
return createDiagnosticForNodeInSourceFileOrCompilerDiagnostic(jsonSourceFile, element, message, spec3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function specToDiagnostic(spec2, disallowTrailingRecursion) {
|
function specToDiagnostic(spec3, disallowTrailingRecursion) {
|
||||||
Debug.assert(typeof spec2 === "string");
|
Debug.assert(typeof spec3 === "string");
|
||||||
if (disallowTrailingRecursion && invalidTrailingRecursionPattern.test(spec2)) {
|
if (disallowTrailingRecursion && invalidTrailingRecursionPattern.test(spec3)) {
|
||||||
return [Diagnostics.File_specification_cannot_end_in_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, spec2];
|
return [Diagnostics.File_specification_cannot_end_in_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, spec3];
|
||||||
} else if (invalidDotDotAfterRecursiveWildcard(spec2)) {
|
} else if (invalidDotDotAfterRecursiveWildcard(spec3)) {
|
||||||
return [Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, spec2];
|
return [Diagnostics.File_specification_cannot_contain_a_parent_directory_that_appears_after_a_recursive_directory_wildcard_Asterisk_Asterisk_Colon_0, spec3];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function getWildcardDirectories({ validatedIncludeSpecs: include, validatedExcludeSpecs: exclude }, basePath, useCaseSensitiveFileNames2) {
|
function getWildcardDirectories({ validatedIncludeSpecs: include, validatedExcludeSpecs: exclude }, basePath, useCaseSensitiveFileNames2) {
|
||||||
@@ -40787,11 +40818,11 @@ ${lanes.join(`
|
|||||||
if (include !== undefined) {
|
if (include !== undefined) {
|
||||||
const recursiveKeys = [];
|
const recursiveKeys = [];
|
||||||
for (const file of include) {
|
for (const file of include) {
|
||||||
const spec2 = normalizePath(combinePaths(basePath, file));
|
const spec3 = normalizePath(combinePaths(basePath, file));
|
||||||
if (excludeRegex && excludeRegex.test(spec2)) {
|
if (excludeRegex && excludeRegex.test(spec3)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const match = getWildcardDirectoryFromSpec(spec2, useCaseSensitiveFileNames2);
|
const match = getWildcardDirectoryFromSpec(spec3, useCaseSensitiveFileNames2);
|
||||||
if (match) {
|
if (match) {
|
||||||
const { key, path, flags } = match;
|
const { key, path, flags } = match;
|
||||||
const existingPath = wildCardKeyToPath.get(key);
|
const existingPath = wildCardKeyToPath.get(key);
|
||||||
@@ -40822,20 +40853,20 @@ ${lanes.join(`
|
|||||||
function toCanonicalKey(path, useCaseSensitiveFileNames2) {
|
function toCanonicalKey(path, useCaseSensitiveFileNames2) {
|
||||||
return useCaseSensitiveFileNames2 ? path : toFileNameLowerCase(path);
|
return useCaseSensitiveFileNames2 ? path : toFileNameLowerCase(path);
|
||||||
}
|
}
|
||||||
function getWildcardDirectoryFromSpec(spec2, useCaseSensitiveFileNames2) {
|
function getWildcardDirectoryFromSpec(spec3, useCaseSensitiveFileNames2) {
|
||||||
const match = wildcardDirectoryPattern.exec(spec2);
|
const match = wildcardDirectoryPattern.exec(spec3);
|
||||||
if (match) {
|
if (match) {
|
||||||
const questionWildcardIndex = spec2.indexOf("?");
|
const questionWildcardIndex = spec3.indexOf("?");
|
||||||
const starWildcardIndex = spec2.indexOf("*");
|
const starWildcardIndex = spec3.indexOf("*");
|
||||||
const lastDirectorySeperatorIndex = spec2.lastIndexOf(directorySeparator);
|
const lastDirectorySeperatorIndex = spec3.lastIndexOf(directorySeparator);
|
||||||
return {
|
return {
|
||||||
key: toCanonicalKey(match[0], useCaseSensitiveFileNames2),
|
key: toCanonicalKey(match[0], useCaseSensitiveFileNames2),
|
||||||
path: match[0],
|
path: match[0],
|
||||||
flags: questionWildcardIndex !== -1 && questionWildcardIndex < lastDirectorySeperatorIndex || starWildcardIndex !== -1 && starWildcardIndex < lastDirectorySeperatorIndex ? 1 : 0
|
flags: questionWildcardIndex !== -1 && questionWildcardIndex < lastDirectorySeperatorIndex || starWildcardIndex !== -1 && starWildcardIndex < lastDirectorySeperatorIndex ? 1 : 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (isImplicitGlob(spec2.substring(spec2.lastIndexOf(directorySeparator) + 1))) {
|
if (isImplicitGlob(spec3.substring(spec3.lastIndexOf(directorySeparator) + 1))) {
|
||||||
const path = removeTrailingDirectorySeparator(spec2);
|
const path = removeTrailingDirectorySeparator(spec3);
|
||||||
return {
|
return {
|
||||||
key: toCanonicalKey(path, useCaseSensitiveFileNames2),
|
key: toCanonicalKey(path, useCaseSensitiveFileNames2),
|
||||||
path,
|
path,
|
||||||
@@ -45962,7 +45993,7 @@ ${lanes.join(`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var ts_moduleSpecifiers_exports = {};
|
var ts_moduleSpecifiers_exports = {};
|
||||||
__export(ts_moduleSpecifiers_exports, {
|
__export2(ts_moduleSpecifiers_exports, {
|
||||||
RelativePreference: () => RelativePreference,
|
RelativePreference: () => RelativePreference,
|
||||||
countPathComponents: () => countPathComponents,
|
countPathComponents: () => countPathComponents,
|
||||||
forEachFileNameOfModule: () => forEachFileNameOfModule,
|
forEachFileNameOfModule: () => forEachFileNameOfModule,
|
||||||
@@ -118075,7 +118106,7 @@ ${lanes.join(`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var ts_JsTyping_exports = {};
|
var ts_JsTyping_exports = {};
|
||||||
__export(ts_JsTyping_exports, {
|
__export2(ts_JsTyping_exports, {
|
||||||
NameValidationResult: () => NameValidationResult,
|
NameValidationResult: () => NameValidationResult,
|
||||||
discoverTypings: () => discoverTypings,
|
discoverTypings: () => discoverTypings,
|
||||||
isTypingUpToDate: () => isTypingUpToDate,
|
isTypingUpToDate: () => isTypingUpToDate,
|
||||||
@@ -121532,8 +121563,8 @@ ${lanes.join(`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
function getIsExcludedPatterns(preferences, useCaseSensitiveFileNames2) {
|
function getIsExcludedPatterns(preferences, useCaseSensitiveFileNames2) {
|
||||||
return mapDefined(preferences.autoImportFileExcludePatterns, (spec2) => {
|
return mapDefined(preferences.autoImportFileExcludePatterns, (spec3) => {
|
||||||
const pattern = getSubPatternFromSpec(spec2, "", "exclude");
|
const pattern = getSubPatternFromSpec(spec3, "", "exclude");
|
||||||
return pattern ? getRegexFromPattern(pattern, useCaseSensitiveFileNames2) : undefined;
|
return pattern ? getRegexFromPattern(pattern, useCaseSensitiveFileNames2) : undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -124366,7 +124397,7 @@ interface Symbol {
|
|||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
var ts_NavigateTo_exports = {};
|
var ts_NavigateTo_exports = {};
|
||||||
__export(ts_NavigateTo_exports, {
|
__export2(ts_NavigateTo_exports, {
|
||||||
getNavigateToItems: () => getNavigateToItems
|
getNavigateToItems: () => getNavigateToItems
|
||||||
});
|
});
|
||||||
function getNavigateToItems(sourceFiles, checker, cancellationToken, searchValue, maxResultCount, excludeDtsFiles, excludeLibFiles, program) {
|
function getNavigateToItems(sourceFiles, checker, cancellationToken, searchValue, maxResultCount, excludeDtsFiles, excludeLibFiles, program) {
|
||||||
@@ -124471,7 +124502,7 @@ interface Symbol {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
var ts_NavigationBar_exports = {};
|
var ts_NavigationBar_exports = {};
|
||||||
__export(ts_NavigationBar_exports, {
|
__export2(ts_NavigationBar_exports, {
|
||||||
getNavigationBarItems: () => getNavigationBarItems,
|
getNavigationBarItems: () => getNavigationBarItems,
|
||||||
getNavigationTree: () => getNavigationTree
|
getNavigationTree: () => getNavigationTree
|
||||||
});
|
});
|
||||||
@@ -125200,7 +125231,7 @@ interface Symbol {
|
|||||||
return text.replace(/\\?(?:\r?\n|[\r\u2028\u2029])/g, "");
|
return text.replace(/\\?(?:\r?\n|[\r\u2028\u2029])/g, "");
|
||||||
}
|
}
|
||||||
var ts_refactor_exports = {};
|
var ts_refactor_exports = {};
|
||||||
__export(ts_refactor_exports, {
|
__export2(ts_refactor_exports, {
|
||||||
addExportsInOldFile: () => addExportsInOldFile,
|
addExportsInOldFile: () => addExportsInOldFile,
|
||||||
addImportsForMovedSymbols: () => addImportsForMovedSymbols,
|
addImportsForMovedSymbols: () => addImportsForMovedSymbols,
|
||||||
addNewFileToTsconfig: () => addNewFileToTsconfig,
|
addNewFileToTsconfig: () => addNewFileToTsconfig,
|
||||||
@@ -125345,8 +125376,8 @@ interface Symbol {
|
|||||||
if (wasDefault) {
|
if (wasDefault) {
|
||||||
if (isExportAssignment(exportNode) && !exportNode.isExportEquals) {
|
if (isExportAssignment(exportNode) && !exportNode.isExportEquals) {
|
||||||
const exp = exportNode.expression;
|
const exp = exportNode.expression;
|
||||||
const spec2 = makeExportSpecifier(exp.text, exp.text);
|
const spec3 = makeExportSpecifier(exp.text, exp.text);
|
||||||
changes.replaceNode(exportingSourceFile, exportNode, factory.createExportDeclaration(undefined, false, factory.createNamedExports([spec2])));
|
changes.replaceNode(exportingSourceFile, exportNode, factory.createExportDeclaration(undefined, false, factory.createNamedExports([spec3])));
|
||||||
} else {
|
} else {
|
||||||
changes.delete(exportingSourceFile, Debug.checkDefined(findModifier(exportNode, 90), "Should find a default keyword in modifier list"));
|
changes.delete(exportingSourceFile, Debug.checkDefined(findModifier(exportNode, 90), "Should find a default keyword in modifier list"));
|
||||||
}
|
}
|
||||||
@@ -125397,17 +125428,17 @@ interface Symbol {
|
|||||||
break;
|
break;
|
||||||
case 277:
|
case 277:
|
||||||
case 282: {
|
case 282: {
|
||||||
const spec2 = parent2;
|
const spec3 = parent2;
|
||||||
changes.replaceNode(importingSourceFile, spec2, makeImportSpecifier(exportName, spec2.name.text));
|
changes.replaceNode(importingSourceFile, spec3, makeImportSpecifier(exportName, spec3.name.text));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 274: {
|
case 274: {
|
||||||
const clause = parent2;
|
const clause = parent2;
|
||||||
Debug.assert(clause.name === ref, "Import clause name should match provided ref");
|
Debug.assert(clause.name === ref, "Import clause name should match provided ref");
|
||||||
const spec2 = makeImportSpecifier(exportName, ref.text);
|
const spec3 = makeImportSpecifier(exportName, ref.text);
|
||||||
const { namedBindings } = clause;
|
const { namedBindings } = clause;
|
||||||
if (!namedBindings) {
|
if (!namedBindings) {
|
||||||
changes.replaceNode(importingSourceFile, ref, factory.createNamedImports([spec2]));
|
changes.replaceNode(importingSourceFile, ref, factory.createNamedImports([spec3]));
|
||||||
} else if (namedBindings.kind === 275) {
|
} else if (namedBindings.kind === 275) {
|
||||||
changes.deleteRange(importingSourceFile, { pos: ref.getStart(importingSourceFile), end: namedBindings.getStart(importingSourceFile) });
|
changes.deleteRange(importingSourceFile, { pos: ref.getStart(importingSourceFile), end: namedBindings.getStart(importingSourceFile) });
|
||||||
const quotePreference = isStringLiteral(clause.parent.moduleSpecifier) ? quotePreferenceFromString(clause.parent.moduleSpecifier, importingSourceFile) : 1;
|
const quotePreference = isStringLiteral(clause.parent.moduleSpecifier) ? quotePreferenceFromString(clause.parent.moduleSpecifier, importingSourceFile) : 1;
|
||||||
@@ -125415,7 +125446,7 @@ interface Symbol {
|
|||||||
changes.insertNodeAfter(importingSourceFile, clause.parent, newImport);
|
changes.insertNodeAfter(importingSourceFile, clause.parent, newImport);
|
||||||
} else {
|
} else {
|
||||||
changes.delete(importingSourceFile, ref);
|
changes.delete(importingSourceFile, ref);
|
||||||
changes.insertNodeAtEndOfList(importingSourceFile, namedBindings.elements, spec2);
|
changes.insertNodeAtEndOfList(importingSourceFile, namedBindings.elements, spec3);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -128196,7 +128227,7 @@ ${newComment.split(`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var ts_refactor_extractSymbol_exports = {};
|
var ts_refactor_extractSymbol_exports = {};
|
||||||
__export(ts_refactor_extractSymbol_exports, {
|
__export2(ts_refactor_extractSymbol_exports, {
|
||||||
Messages: () => Messages,
|
Messages: () => Messages,
|
||||||
RangeFacts: () => RangeFacts,
|
RangeFacts: () => RangeFacts,
|
||||||
getRangeToExtract: () => getRangeToExtract2,
|
getRangeToExtract: () => getRangeToExtract2,
|
||||||
@@ -132122,7 +132153,7 @@ ${newComment.split(`
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
var ts_BreakpointResolver_exports = {};
|
var ts_BreakpointResolver_exports = {};
|
||||||
__export(ts_BreakpointResolver_exports, {
|
__export2(ts_BreakpointResolver_exports, {
|
||||||
spanInSourceFileAtLocation: () => spanInSourceFileAtLocation
|
spanInSourceFileAtLocation: () => spanInSourceFileAtLocation
|
||||||
});
|
});
|
||||||
function spanInSourceFileAtLocation(sourceFile, position) {
|
function spanInSourceFileAtLocation(sourceFile, position) {
|
||||||
@@ -132600,7 +132631,7 @@ ${newComment.split(`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var ts_CallHierarchy_exports = {};
|
var ts_CallHierarchy_exports = {};
|
||||||
__export(ts_CallHierarchy_exports, {
|
__export2(ts_CallHierarchy_exports, {
|
||||||
createCallHierarchyItem: () => createCallHierarchyItem,
|
createCallHierarchyItem: () => createCallHierarchyItem,
|
||||||
getIncomingCalls: () => getIncomingCalls,
|
getIncomingCalls: () => getIncomingCalls,
|
||||||
getOutgoingCalls: () => getOutgoingCalls,
|
getOutgoingCalls: () => getOutgoingCalls,
|
||||||
@@ -133009,11 +133040,11 @@ ${newComment.split(`
|
|||||||
return group(collectCallSites(program, declaration), getCallSiteGroupKey, (entries) => convertCallSiteGroupToOutgoingCall(program, entries));
|
return group(collectCallSites(program, declaration), getCallSiteGroupKey, (entries) => convertCallSiteGroupToOutgoingCall(program, entries));
|
||||||
}
|
}
|
||||||
var ts_classifier_exports = {};
|
var ts_classifier_exports = {};
|
||||||
__export(ts_classifier_exports, {
|
__export2(ts_classifier_exports, {
|
||||||
v2020: () => ts_classifier_v2020_exports
|
v2020: () => ts_classifier_v2020_exports
|
||||||
});
|
});
|
||||||
var ts_classifier_v2020_exports = {};
|
var ts_classifier_v2020_exports = {};
|
||||||
__export(ts_classifier_v2020_exports, {
|
__export2(ts_classifier_v2020_exports, {
|
||||||
TokenEncodingConsts: () => TokenEncodingConsts,
|
TokenEncodingConsts: () => TokenEncodingConsts,
|
||||||
TokenModifier: () => TokenModifier,
|
TokenModifier: () => TokenModifier,
|
||||||
TokenType: () => TokenType,
|
TokenType: () => TokenType,
|
||||||
@@ -133021,7 +133052,7 @@ ${newComment.split(`
|
|||||||
getSemanticClassifications: () => getSemanticClassifications2
|
getSemanticClassifications: () => getSemanticClassifications2
|
||||||
});
|
});
|
||||||
var ts_codefix_exports = {};
|
var ts_codefix_exports = {};
|
||||||
__export(ts_codefix_exports, {
|
__export2(ts_codefix_exports, {
|
||||||
PreserveOptionalFlags: () => PreserveOptionalFlags,
|
PreserveOptionalFlags: () => PreserveOptionalFlags,
|
||||||
addNewNodeForMemberSymbol: () => addNewNodeForMemberSymbol,
|
addNewNodeForMemberSymbol: () => addNewNodeForMemberSymbol,
|
||||||
codeFixAll: () => codeFixAll,
|
codeFixAll: () => codeFixAll,
|
||||||
@@ -136454,13 +136485,13 @@ ${newComment.split(`
|
|||||||
changes.replaceNode(sourceFile, clause.namedBindings, factory.updateNamedImports(clause.namedBindings, toSorted([...existingSpecifiers.filter((s) => !removeExistingImportSpecifiers.has(s)), ...newSpecifiers], specifierComparer)));
|
changes.replaceNode(sourceFile, clause.namedBindings, factory.updateNamedImports(clause.namedBindings, toSorted([...existingSpecifiers.filter((s) => !removeExistingImportSpecifiers.has(s)), ...newSpecifiers], specifierComparer)));
|
||||||
} else if ((existingSpecifiers == null ? undefined : existingSpecifiers.length) && isSorted !== false) {
|
} else if ((existingSpecifiers == null ? undefined : existingSpecifiers.length) && isSorted !== false) {
|
||||||
const transformedExistingSpecifiers = promoteFromTypeOnly2 && existingSpecifiers ? factory.updateNamedImports(clause.namedBindings, sameMap(existingSpecifiers, (e) => factory.updateImportSpecifier(e, true, e.propertyName, e.name))).elements : existingSpecifiers;
|
const transformedExistingSpecifiers = promoteFromTypeOnly2 && existingSpecifiers ? factory.updateNamedImports(clause.namedBindings, sameMap(existingSpecifiers, (e) => factory.updateImportSpecifier(e, true, e.propertyName, e.name))).elements : existingSpecifiers;
|
||||||
for (const spec2 of newSpecifiers) {
|
for (const spec3 of newSpecifiers) {
|
||||||
const insertionIndex = ts_OrganizeImports_exports.getImportSpecifierInsertionIndex(transformedExistingSpecifiers, spec2, specifierComparer);
|
const insertionIndex = ts_OrganizeImports_exports.getImportSpecifierInsertionIndex(transformedExistingSpecifiers, spec3, specifierComparer);
|
||||||
changes.insertImportSpecifierAtIndex(sourceFile, spec2, clause.namedBindings, insertionIndex);
|
changes.insertImportSpecifierAtIndex(sourceFile, spec3, clause.namedBindings, insertionIndex);
|
||||||
}
|
}
|
||||||
} else if (existingSpecifiers == null ? undefined : existingSpecifiers.length) {
|
} else if (existingSpecifiers == null ? undefined : existingSpecifiers.length) {
|
||||||
for (const spec2 of newSpecifiers) {
|
for (const spec3 of newSpecifiers) {
|
||||||
changes.insertNodeInListAfter(sourceFile, last(existingSpecifiers), spec2, existingSpecifiers);
|
changes.insertNodeInListAfter(sourceFile, last(existingSpecifiers), spec3, existingSpecifiers);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (newSpecifiers.length) {
|
if (newSpecifiers.length) {
|
||||||
@@ -142220,7 +142251,7 @@ ${newComment.split(`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var ts_Completions_exports = {};
|
var ts_Completions_exports = {};
|
||||||
__export(ts_Completions_exports, {
|
__export2(ts_Completions_exports, {
|
||||||
CompletionKind: () => CompletionKind,
|
CompletionKind: () => CompletionKind,
|
||||||
CompletionSource: () => CompletionSource,
|
CompletionSource: () => CompletionSource,
|
||||||
SortText: () => SortText,
|
SortText: () => SortText,
|
||||||
@@ -145770,7 +145801,7 @@ ${newComment.split(`
|
|||||||
return keyword === "abstract" || keyword === "async" || keyword === "await" || keyword === "declare" || keyword === "module" || keyword === "namespace" || keyword === "type" || keyword === "satisfies" || keyword === "as";
|
return keyword === "abstract" || keyword === "async" || keyword === "await" || keyword === "declare" || keyword === "module" || keyword === "namespace" || keyword === "type" || keyword === "satisfies" || keyword === "as";
|
||||||
}
|
}
|
||||||
var ts_Completions_StringCompletions_exports = {};
|
var ts_Completions_StringCompletions_exports = {};
|
||||||
__export(ts_Completions_StringCompletions_exports, {
|
__export2(ts_Completions_StringCompletions_exports, {
|
||||||
getStringLiteralCompletionDetails: () => getStringLiteralCompletionDetails,
|
getStringLiteralCompletionDetails: () => getStringLiteralCompletionDetails,
|
||||||
getStringLiteralCompletions: () => getStringLiteralCompletions
|
getStringLiteralCompletions: () => getStringLiteralCompletions
|
||||||
});
|
});
|
||||||
@@ -146603,7 +146634,7 @@ ${newComment.split(`
|
|||||||
return isCallExpression(node.parent) && firstOrUndefined(node.parent.arguments) === node && isIdentifier(node.parent.expression) && node.parent.expression.escapedText === "require";
|
return isCallExpression(node.parent) && firstOrUndefined(node.parent.arguments) === node && isIdentifier(node.parent.expression) && node.parent.expression.escapedText === "require";
|
||||||
}
|
}
|
||||||
var ts_FindAllReferences_exports = {};
|
var ts_FindAllReferences_exports = {};
|
||||||
__export(ts_FindAllReferences_exports, {
|
__export2(ts_FindAllReferences_exports, {
|
||||||
Core: () => Core,
|
Core: () => Core,
|
||||||
DefinitionKind: () => DefinitionKind,
|
DefinitionKind: () => DefinitionKind,
|
||||||
EntryKind: () => EntryKind,
|
EntryKind: () => EntryKind,
|
||||||
@@ -148818,7 +148849,7 @@ ${newComment.split(`
|
|||||||
}
|
}
|
||||||
})(Core || (Core = {}));
|
})(Core || (Core = {}));
|
||||||
var ts_GoToDefinition_exports = {};
|
var ts_GoToDefinition_exports = {};
|
||||||
__export(ts_GoToDefinition_exports, {
|
__export2(ts_GoToDefinition_exports, {
|
||||||
createDefinitionInfo: () => createDefinitionInfo,
|
createDefinitionInfo: () => createDefinitionInfo,
|
||||||
getDefinitionAndBoundSpan: () => getDefinitionAndBoundSpan,
|
getDefinitionAndBoundSpan: () => getDefinitionAndBoundSpan,
|
||||||
getDefinitionAtPosition: () => getDefinitionAtPosition,
|
getDefinitionAtPosition: () => getDefinitionAtPosition,
|
||||||
@@ -149314,7 +149345,7 @@ ${newComment.split(`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var ts_InlayHints_exports = {};
|
var ts_InlayHints_exports = {};
|
||||||
__export(ts_InlayHints_exports, {
|
__export2(ts_InlayHints_exports, {
|
||||||
provideInlayHints: () => provideInlayHints
|
provideInlayHints: () => provideInlayHints
|
||||||
});
|
});
|
||||||
var leadingParameterNameCommentRegexFactory = (name) => {
|
var leadingParameterNameCommentRegexFactory = (name) => {
|
||||||
@@ -150076,7 +150107,7 @@ ${newComment.split(`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var ts_JsDoc_exports = {};
|
var ts_JsDoc_exports = {};
|
||||||
__export(ts_JsDoc_exports, {
|
__export2(ts_JsDoc_exports, {
|
||||||
getDocCommentTemplateAtPosition: () => getDocCommentTemplateAtPosition,
|
getDocCommentTemplateAtPosition: () => getDocCommentTemplateAtPosition,
|
||||||
getJSDocParameterNameCompletionDetails: () => getJSDocParameterNameCompletionDetails,
|
getJSDocParameterNameCompletionDetails: () => getJSDocParameterNameCompletionDetails,
|
||||||
getJSDocParameterNameCompletions: () => getJSDocParameterNameCompletions,
|
getJSDocParameterNameCompletions: () => getJSDocParameterNameCompletions,
|
||||||
@@ -150496,7 +150527,7 @@ ${newComment.split(`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var ts_MapCode_exports = {};
|
var ts_MapCode_exports = {};
|
||||||
__export(ts_MapCode_exports, {
|
__export2(ts_MapCode_exports, {
|
||||||
mapCode: () => mapCode
|
mapCode: () => mapCode
|
||||||
});
|
});
|
||||||
function mapCode(sourceFile, contents, focusLocations, host, formatContext, preferences) {
|
function mapCode(sourceFile, contents, focusLocations, host, formatContext, preferences) {
|
||||||
@@ -150631,7 +150662,7 @@ ${content}
|
|||||||
node.forEachChild(resetNodePositions);
|
node.forEachChild(resetNodePositions);
|
||||||
}
|
}
|
||||||
var ts_OrganizeImports_exports = {};
|
var ts_OrganizeImports_exports = {};
|
||||||
__export(ts_OrganizeImports_exports, {
|
__export2(ts_OrganizeImports_exports, {
|
||||||
compareImportsOrRequireStatements: () => compareImportsOrRequireStatements,
|
compareImportsOrRequireStatements: () => compareImportsOrRequireStatements,
|
||||||
compareModuleSpecifiers: () => compareModuleSpecifiers2,
|
compareModuleSpecifiers: () => compareModuleSpecifiers2,
|
||||||
getImportDeclarationInsertionIndex: () => getImportDeclarationInsertionIndex,
|
getImportDeclarationInsertionIndex: () => getImportDeclarationInsertionIndex,
|
||||||
@@ -151214,7 +151245,7 @@ ${content}
|
|||||||
return compareModuleSpecifiersWorker(m1, m2, comparer);
|
return compareModuleSpecifiersWorker(m1, m2, comparer);
|
||||||
}
|
}
|
||||||
var ts_OutliningElementsCollector_exports = {};
|
var ts_OutliningElementsCollector_exports = {};
|
||||||
__export(ts_OutliningElementsCollector_exports, {
|
__export2(ts_OutliningElementsCollector_exports, {
|
||||||
collectElements: () => collectElements
|
collectElements: () => collectElements
|
||||||
});
|
});
|
||||||
function collectElements(sourceFile, cancellationToken) {
|
function collectElements(sourceFile, cancellationToken) {
|
||||||
@@ -151532,7 +151563,7 @@ ${content}
|
|||||||
return findChildOfKind(body, 19, sourceFile);
|
return findChildOfKind(body, 19, sourceFile);
|
||||||
}
|
}
|
||||||
var ts_Rename_exports = {};
|
var ts_Rename_exports = {};
|
||||||
__export(ts_Rename_exports, {
|
__export2(ts_Rename_exports, {
|
||||||
getRenameInfo: () => getRenameInfo,
|
getRenameInfo: () => getRenameInfo,
|
||||||
nodeIsEligibleForRename: () => nodeIsEligibleForRename
|
nodeIsEligibleForRename: () => nodeIsEligibleForRename
|
||||||
});
|
});
|
||||||
@@ -151686,7 +151717,7 @@ ${content}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var ts_SignatureHelp_exports = {};
|
var ts_SignatureHelp_exports = {};
|
||||||
__export(ts_SignatureHelp_exports, {
|
__export2(ts_SignatureHelp_exports, {
|
||||||
getArgumentInfoForCompletions: () => getArgumentInfoForCompletions,
|
getArgumentInfoForCompletions: () => getArgumentInfoForCompletions,
|
||||||
getSignatureHelpItems: () => getSignatureHelpItems
|
getSignatureHelpItems: () => getSignatureHelpItems
|
||||||
});
|
});
|
||||||
@@ -152160,7 +152191,7 @@ ${content}
|
|||||||
return { name: typeParameter.symbol.name, documentation: typeParameter.symbol.getDocumentationComment(checker), displayParts, isOptional: false, isRest: false };
|
return { name: typeParameter.symbol.name, documentation: typeParameter.symbol.getDocumentationComment(checker), displayParts, isOptional: false, isRest: false };
|
||||||
}
|
}
|
||||||
var ts_SmartSelectionRange_exports = {};
|
var ts_SmartSelectionRange_exports = {};
|
||||||
__export(ts_SmartSelectionRange_exports, {
|
__export2(ts_SmartSelectionRange_exports, {
|
||||||
getSmartSelectionRange: () => getSmartSelectionRange
|
getSmartSelectionRange: () => getSmartSelectionRange
|
||||||
});
|
});
|
||||||
function getSmartSelectionRange(pos, sourceFile) {
|
function getSmartSelectionRange(pos, sourceFile) {
|
||||||
@@ -152352,7 +152383,7 @@ ${content}
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var ts_SymbolDisplay_exports = {};
|
var ts_SymbolDisplay_exports = {};
|
||||||
__export(ts_SymbolDisplay_exports, {
|
__export2(ts_SymbolDisplay_exports, {
|
||||||
getSymbolDisplayPartsDocumentationAndSymbolKind: () => getSymbolDisplayPartsDocumentationAndSymbolKind,
|
getSymbolDisplayPartsDocumentationAndSymbolKind: () => getSymbolDisplayPartsDocumentationAndSymbolKind,
|
||||||
getSymbolKind: () => getSymbolKind,
|
getSymbolKind: () => getSymbolKind,
|
||||||
getSymbolModifiers: () => getSymbolModifiers
|
getSymbolModifiers: () => getSymbolModifiers
|
||||||
@@ -153058,7 +153089,7 @@ ${content}
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
var ts_textChanges_exports = {};
|
var ts_textChanges_exports = {};
|
||||||
__export(ts_textChanges_exports, {
|
__export2(ts_textChanges_exports, {
|
||||||
ChangeTracker: () => ChangeTracker,
|
ChangeTracker: () => ChangeTracker,
|
||||||
LeadingTriviaOption: () => LeadingTriviaOption,
|
LeadingTriviaOption: () => LeadingTriviaOption,
|
||||||
TrailingTriviaOption: () => TrailingTriviaOption,
|
TrailingTriviaOption: () => TrailingTriviaOption,
|
||||||
@@ -154255,7 +154286,7 @@ ${options.prefix}` : `
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
var ts_formatting_exports = {};
|
var ts_formatting_exports = {};
|
||||||
__export(ts_formatting_exports, {
|
__export2(ts_formatting_exports, {
|
||||||
FormattingContext: () => FormattingContext,
|
FormattingContext: () => FormattingContext,
|
||||||
FormattingRequestKind: () => FormattingRequestKind,
|
FormattingRequestKind: () => FormattingRequestKind,
|
||||||
RuleAction: () => RuleAction,
|
RuleAction: () => RuleAction,
|
||||||
@@ -156640,7 +156671,7 @@ ${options.prefix}` : `
|
|||||||
}
|
}
|
||||||
})(SmartIndenter || (SmartIndenter = {}));
|
})(SmartIndenter || (SmartIndenter = {}));
|
||||||
var ts_preparePasteEdits_exports = {};
|
var ts_preparePasteEdits_exports = {};
|
||||||
__export(ts_preparePasteEdits_exports, {
|
__export2(ts_preparePasteEdits_exports, {
|
||||||
preparePasteEdits: () => preparePasteEdits
|
preparePasteEdits: () => preparePasteEdits
|
||||||
});
|
});
|
||||||
function preparePasteEdits(sourceFile, copiedFromRange, checker) {
|
function preparePasteEdits(sourceFile, copiedFromRange, checker) {
|
||||||
@@ -156672,7 +156703,7 @@ ${options.prefix}` : `
|
|||||||
return shouldProvidePasteEdits;
|
return shouldProvidePasteEdits;
|
||||||
}
|
}
|
||||||
var ts_PasteEdits_exports = {};
|
var ts_PasteEdits_exports = {};
|
||||||
__export(ts_PasteEdits_exports, {
|
__export2(ts_PasteEdits_exports, {
|
||||||
pasteEditsProvider: () => pasteEditsProvider
|
pasteEditsProvider: () => pasteEditsProvider
|
||||||
});
|
});
|
||||||
var fixId55 = "providePostPasteEdits";
|
var fixId55 = "providePostPasteEdits";
|
||||||
@@ -156763,7 +156794,7 @@ ${options.prefix}` : `
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
var ts_exports2 = {};
|
var ts_exports2 = {};
|
||||||
__export(ts_exports2, {
|
__export2(ts_exports2, {
|
||||||
ANONYMOUS: () => ANONYMOUS,
|
ANONYMOUS: () => ANONYMOUS,
|
||||||
AccessFlags: () => AccessFlags,
|
AccessFlags: () => AccessFlags,
|
||||||
AssertionLevel: () => AssertionLevel,
|
AssertionLevel: () => AssertionLevel,
|
||||||
@@ -159104,7 +159135,7 @@ ${options.prefix}` : `
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
var ts_server_exports3 = {};
|
var ts_server_exports3 = {};
|
||||||
__export(ts_server_exports3, {
|
__export2(ts_server_exports3, {
|
||||||
ActionInvalidate: () => ActionInvalidate,
|
ActionInvalidate: () => ActionInvalidate,
|
||||||
ActionPackageInstalled: () => ActionPackageInstalled,
|
ActionPackageInstalled: () => ActionPackageInstalled,
|
||||||
ActionSet: () => ActionSet,
|
ActionSet: () => ActionSet,
|
||||||
@@ -159202,7 +159233,7 @@ ${options.prefix}` : `
|
|||||||
updateProjectIfDirty: () => updateProjectIfDirty
|
updateProjectIfDirty: () => updateProjectIfDirty
|
||||||
});
|
});
|
||||||
var ts_server_typingsInstaller_exports = {};
|
var ts_server_typingsInstaller_exports = {};
|
||||||
__export(ts_server_typingsInstaller_exports, {
|
__export2(ts_server_typingsInstaller_exports, {
|
||||||
TypingsInstaller: () => TypingsInstaller,
|
TypingsInstaller: () => TypingsInstaller,
|
||||||
getNpmCommandForInstallation: () => getNpmCommandForInstallation,
|
getNpmCommandForInstallation: () => getNpmCommandForInstallation,
|
||||||
installNpmPackages: () => installNpmPackages,
|
installNpmPackages: () => installNpmPackages,
|
||||||
@@ -159757,7 +159788,7 @@ ${options.prefix}` : `
|
|||||||
return base === "tsconfig.json" || base === "jsconfig.json" ? base : undefined;
|
return base === "tsconfig.json" || base === "jsconfig.json" ? base : undefined;
|
||||||
}
|
}
|
||||||
var ts_server_protocol_exports = {};
|
var ts_server_protocol_exports = {};
|
||||||
__export(ts_server_protocol_exports, {
|
__export2(ts_server_protocol_exports, {
|
||||||
ClassificationType: () => ClassificationType,
|
ClassificationType: () => ClassificationType,
|
||||||
CommandTypes: () => CommandTypes,
|
CommandTypes: () => CommandTypes,
|
||||||
CompletionTriggerKind: () => CompletionTriggerKind,
|
CompletionTriggerKind: () => CompletionTriggerKind,
|
||||||
@@ -169359,7 +169390,7 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range
|
|||||||
_TypingsInstallerAdapter.requestDelayMillis = 100;
|
_TypingsInstallerAdapter.requestDelayMillis = 100;
|
||||||
var TypingsInstallerAdapter = _TypingsInstallerAdapter;
|
var TypingsInstallerAdapter = _TypingsInstallerAdapter;
|
||||||
var ts_server_exports4 = {};
|
var ts_server_exports4 = {};
|
||||||
__export(ts_server_exports4, {
|
__export2(ts_server_exports4, {
|
||||||
ActionInvalidate: () => ActionInvalidate,
|
ActionInvalidate: () => ActionInvalidate,
|
||||||
ActionPackageInstalled: () => ActionPackageInstalled,
|
ActionPackageInstalled: () => ActionPackageInstalled,
|
||||||
ActionSet: () => ActionSet,
|
ActionSet: () => ActionSet,
|
||||||
@@ -169485,10 +169516,10 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range
|
|||||||
// node_modules/.bun/@vscode+l10n@0.0.18/node_modules/@vscode/l10n/dist/main.js
|
// node_modules/.bun/@vscode+l10n@0.0.18/node_modules/@vscode/l10n/dist/main.js
|
||||||
var require_main = __commonJS((exports2, module2) => {
|
var require_main = __commonJS((exports2, module2) => {
|
||||||
var __defProp2 = Object.defineProperty;
|
var __defProp2 = Object.defineProperty;
|
||||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
|
||||||
var __getOwnPropNames2 = Object.getOwnPropertyNames;
|
var __getOwnPropNames2 = Object.getOwnPropertyNames;
|
||||||
var __hasOwnProp2 = Object.prototype.hasOwnProperty;
|
var __hasOwnProp2 = Object.prototype.hasOwnProperty;
|
||||||
var __export = (target, all) => {
|
var __export2 = (target, all) => {
|
||||||
for (var name in all)
|
for (var name in all)
|
||||||
__defProp2(target, name, { get: all[name], enumerable: true });
|
__defProp2(target, name, { get: all[name], enumerable: true });
|
||||||
};
|
};
|
||||||
@@ -169496,17 +169527,17 @@ var require_main = __commonJS((exports2, module2) => {
|
|||||||
if (from && typeof from === "object" || typeof from === "function") {
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
for (let key of __getOwnPropNames2(from))
|
for (let key of __getOwnPropNames2(from))
|
||||||
if (!__hasOwnProp2.call(to, key) && key !== except)
|
if (!__hasOwnProp2.call(to, key) && key !== except)
|
||||||
__defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
__defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
|
||||||
}
|
}
|
||||||
return to;
|
return to;
|
||||||
};
|
};
|
||||||
var __toCommonJS = (mod) => __copyProps(__defProp2({}, "__esModule", { value: true }), mod);
|
var __toCommonJS2 = (mod) => __copyProps(__defProp2({}, "__esModule", { value: true }), mod);
|
||||||
var main_exports = {};
|
var main_exports = {};
|
||||||
__export(main_exports, {
|
__export2(main_exports, {
|
||||||
config: () => config,
|
config: () => config,
|
||||||
t: () => t
|
t: () => t
|
||||||
});
|
});
|
||||||
module2.exports = __toCommonJS(main_exports);
|
module2.exports = __toCommonJS2(main_exports);
|
||||||
var import_fs = require("fs");
|
var import_fs = require("fs");
|
||||||
var import_promises = require("fs/promises");
|
var import_promises = require("fs/promises");
|
||||||
async function readFileFromUri(uri) {
|
async function readFileFromUri(uri) {
|
||||||
@@ -169624,6 +169655,14 @@ var require_main = __commonJS((exports2, module2) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// packages/language-server/src/server.ts
|
||||||
|
var exports_server = {};
|
||||||
|
__export(exports_server, {
|
||||||
|
apiCallHover: () => apiCallHover,
|
||||||
|
apiCallCompletions: () => apiCallCompletions
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(exports_server);
|
||||||
|
|
||||||
// packages/syntax/src/tokenizer.ts
|
// packages/syntax/src/tokenizer.ts
|
||||||
class LexError extends Error {
|
class LexError extends Error {
|
||||||
}
|
}
|
||||||
@@ -196164,6 +196203,46 @@ function clearHtmlRegionCache(uri) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// packages/language-server/src/server.ts
|
// packages/language-server/src/server.ts
|
||||||
|
function dataApisOf(source) {
|
||||||
|
try {
|
||||||
|
return parse(source).dataApis;
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function apiDetail(block) {
|
||||||
|
return `${block.method} ${block.path}`;
|
||||||
|
}
|
||||||
|
function apiCallCompletions(source) {
|
||||||
|
return dataApisOf(source).map((block) => ({
|
||||||
|
label: block.name,
|
||||||
|
kind: 2,
|
||||||
|
detail: apiDetail(block),
|
||||||
|
documentation: block.sections ? [
|
||||||
|
block.sections.body.length ? `body: ${block.sections.body.map((field) => `${field.name}${field.optional ? "?" : ""}: ${field.type}`).join(", ")}` : undefined
|
||||||
|
].filter(Boolean).join(`
|
||||||
|
`) : undefined
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
function apiCallHover(source, name) {
|
||||||
|
const block = dataApisOf(source).find((entry) => entry.name === name);
|
||||||
|
if (!block)
|
||||||
|
return;
|
||||||
|
const lines = [`**${block.name}** \`${block.method} ${block.path}\``];
|
||||||
|
if (block.sections?.body.length) {
|
||||||
|
lines.push(`request body: ${block.sections.body.map((field) => `${field.name}${field.optional ? "?" : ""}: ${field.type}`).join(", ")}`);
|
||||||
|
}
|
||||||
|
if (block.sections?.parameters.length) {
|
||||||
|
lines.push(`parameters: ${block.sections.parameters.map((field) => `${field.name}${field.optional ? "?" : ""}: ${field.type}`).join(", ")}`);
|
||||||
|
}
|
||||||
|
return lines.join(`
|
||||||
|
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
function isApiCallPosition(text, offset) {
|
||||||
|
const before = text.slice(0, offset);
|
||||||
|
return /\bapi\.\w*$/.test(before);
|
||||||
|
}
|
||||||
var documents = new Map;
|
var documents = new Map;
|
||||||
var diagnosticTimers = new Map;
|
var diagnosticTimers = new Map;
|
||||||
var MAX_OPEN_DOCUMENTS = 256;
|
var MAX_OPEN_DOCUMENTS = 256;
|
||||||
@@ -196336,6 +196415,10 @@ async function handle(message) {
|
|||||||
}
|
}
|
||||||
case "textDocument/completion": {
|
case "textDocument/completion": {
|
||||||
const document = documents.get(params.textDocument.uri);
|
const document = documents.get(params.textDocument.uri);
|
||||||
|
if (document && isApiCallPosition(document.text, offsetAt(document.text, params.position))) {
|
||||||
|
result(message.id, apiCallCompletions(document.text));
|
||||||
|
break;
|
||||||
|
}
|
||||||
const wrnexus = [...completionItems(), ...workspaceCompletionItems(workspaceRoot)];
|
const wrnexus = [...completionItems(), ...workspaceCompletionItems(workspaceRoot)];
|
||||||
const html = document ? htmlCompletions(document, params.position) : [];
|
const html = document ? htmlCompletions(document, params.position) : [];
|
||||||
result(message.id, html.length ? mergeCompletions(wrnexus, html) : wrnexus);
|
result(message.id, html.length ? mergeCompletions(wrnexus, html) : wrnexus);
|
||||||
@@ -196353,6 +196436,12 @@ async function handle(message) {
|
|||||||
}
|
}
|
||||||
case "textDocument/hover": {
|
case "textDocument/hover": {
|
||||||
const document = documents.get(params.textDocument.uri);
|
const document = documents.get(params.textDocument.uri);
|
||||||
|
const word = document ? wordAt(document.text, params.position)?.word : undefined;
|
||||||
|
const apiHover = word ? apiCallHover(document.text, word) : undefined;
|
||||||
|
if (apiHover) {
|
||||||
|
result(message.id, { contents: apiHover });
|
||||||
|
break;
|
||||||
|
}
|
||||||
const html = document ? htmlHover(document, params.position) : null;
|
const html = document ? htmlHover(document, params.position) : null;
|
||||||
result(message.id, html ?? (document ? hover(document, params.position) : null));
|
result(message.id, html ?? (document ? hover(document, params.position) : null));
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -106,9 +106,6 @@ test('REGRESSION: runtime = "client" still highlights via execution-decl', () =>
|
|||||||
|
|
||||||
test("REGRESSION: the client function modifier still highlights via functions-block", () => {
|
test("REGRESSION: the client function modifier still highlights via functions-block", () => {
|
||||||
const rule = ruleByTopLevelBlockName("functions-block");
|
const rule = ruleByTopLevelBlockName("functions-block");
|
||||||
const modifierPattern = rule.patterns.find(
|
|
||||||
(p) => (p.match && /storage\.modifier\.async/.test("")) || p.match,
|
|
||||||
);
|
|
||||||
const matchRe = new RegExp(
|
const matchRe = new RegExp(
|
||||||
rule.patterns.find(
|
rule.patterns.find(
|
||||||
(p) => p.captures && p.captures["1"]?.name === "storage.modifier.runtime.wrn",
|
(p) => p.captures && p.captures["1"]?.name === "storage.modifier.runtime.wrn",
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
dist/
|
||||||
|
.wrnexus/
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite-shm
|
||||||
|
*.sqlite-wal
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# Northstar CRM example
|
||||||
|
|
||||||
|
A complete WrNexus example covering public pages, SQLite migrations and seed data, SQL-backed signup/login/session authentication, database-backed roles and permissions, protected pages, and permission-checked CRM APIs.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run db:migrate
|
||||||
|
bun run db:seed
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `/signup`, create an account, and the signup hook assigns the `sales-rep` role. The user is automatically signed in and redirected to `/dashboard`. Auth tables come from the `@wrnexus/auth` plugin migrations; CRM and authorization tables come from `app/db/migrations/001_crm.sql`.
|
||||||
|
|
||||||
|
The seed records use the placeholder owner `demo-owner` to demonstrate repeatable data. To attach them to a registered user, update their `owner_id` to that user's ID.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { can } from "@wrnexus/authz";
|
||||||
|
import { getDb } from "@wrnexus/db";
|
||||||
|
import type { Context } from "@wrnexus/core";
|
||||||
|
import { ensureCrmWorkspace } from "../lib/workspace.ts";
|
||||||
|
|
||||||
|
const userId = (ctx: Context) => String((ctx.user as { id?: unknown } | undefined)?.id ?? "");
|
||||||
|
|
||||||
|
export async function GET(ctx: Context): Promise<Response> {
|
||||||
|
if (!(await can(ctx, "contact:read")))
|
||||||
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
||||||
|
await ensureCrmWorkspace(userId(ctx));
|
||||||
|
const rows = await getDb().all(
|
||||||
|
"SELECT id,name,email,company,phone,status,created_at FROM crm_contacts WHERE owner_id = ? ORDER BY name",
|
||||||
|
[userId(ctx)],
|
||||||
|
);
|
||||||
|
return Response.json({ contacts: rows });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(ctx: Context): Promise<Response> {
|
||||||
|
if (!(await can(ctx, "contact:write")))
|
||||||
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
||||||
|
const body = (await ctx.req.json()) as Record<string, unknown>;
|
||||||
|
const name = String(body.name ?? "").trim();
|
||||||
|
const email = String(body.email ?? "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
if (!name || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||||
|
return Response.json({ error: "A name and valid email are required" }, { status: 400 });
|
||||||
|
}
|
||||||
|
const result = await getDb().exec(
|
||||||
|
"INSERT INTO crm_contacts (owner_id,name,email,company,phone,status) VALUES (?,?,?,?,?,?)",
|
||||||
|
[userId(ctx), name, email, String(body.company ?? ""), String(body.phone ?? ""), "lead"],
|
||||||
|
);
|
||||||
|
return Response.json({ id: Number(result.lastInsertId), name, email }, { status: 201 });
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { can } from "@wrnexus/authz";
|
||||||
|
import { getDb } from "@wrnexus/db";
|
||||||
|
import type { Context } from "@wrnexus/core";
|
||||||
|
import { ensureCrmWorkspace } from "../lib/workspace.ts";
|
||||||
|
|
||||||
|
export async function GET(ctx: Context): Promise<Response> {
|
||||||
|
if (!(await can(ctx, "crm:dashboard")))
|
||||||
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
||||||
|
const owner = String((ctx.user as { id?: unknown } | undefined)?.id ?? "");
|
||||||
|
await ensureCrmWorkspace(owner);
|
||||||
|
const metrics = await getDb().one<{
|
||||||
|
pipeline_value_cents: number;
|
||||||
|
open_opportunities: number;
|
||||||
|
contacts: number;
|
||||||
|
}>(
|
||||||
|
`SELECT
|
||||||
|
COALESCE((SELECT SUM(value_cents) FROM crm_deals WHERE owner_id = ? AND stage NOT IN ('won','lost')), 0) pipeline_value_cents,
|
||||||
|
(SELECT COUNT(*) FROM crm_deals WHERE owner_id = ? AND stage NOT IN ('won','lost')) open_opportunities,
|
||||||
|
(SELECT COUNT(*) FROM crm_contacts WHERE owner_id = ?) contacts`,
|
||||||
|
[owner, owner, owner],
|
||||||
|
);
|
||||||
|
return Response.json({
|
||||||
|
metrics: {
|
||||||
|
pipelineValue: new Intl.NumberFormat("en-US", {
|
||||||
|
style: "currency",
|
||||||
|
currency: "USD",
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
}).format(Number(metrics?.pipeline_value_cents ?? 0) / 100),
|
||||||
|
openOpportunities: Number(metrics?.open_opportunities ?? 0),
|
||||||
|
contacts: Number(metrics?.contacts ?? 0),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { can } from "@wrnexus/authz";
|
||||||
|
import { getDb } from "@wrnexus/db";
|
||||||
|
import type { Context } from "@wrnexus/core";
|
||||||
|
import { ensureCrmWorkspace } from "../lib/workspace.ts";
|
||||||
|
|
||||||
|
export async function GET(ctx: Context): Promise<Response> {
|
||||||
|
if (!(await can(ctx, "deal:read"))) return Response.json({ error: "Forbidden" }, { status: 403 });
|
||||||
|
const owner = String((ctx.user as { id?: unknown } | undefined)?.id ?? "");
|
||||||
|
await ensureCrmWorkspace(owner);
|
||||||
|
const deals = await getDb().all(
|
||||||
|
"SELECT d.id,d.title,d.value_cents,d.stage,d.close_date,c.name contact_name FROM crm_deals d LEFT JOIN crm_contacts c ON c.id=d.contact_id WHERE d.owner_id=? ORDER BY d.updated_at DESC",
|
||||||
|
[owner],
|
||||||
|
);
|
||||||
|
return Response.json({ deals });
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { defineAuthz } from "@wrnexus/authz";
|
||||||
|
|
||||||
|
export default defineAuthz({
|
||||||
|
permissions: {
|
||||||
|
"crm:dashboard": { title: "View dashboard" },
|
||||||
|
"contact:read": { title: "View contacts" },
|
||||||
|
"contact:write": { title: "Create and edit contacts" },
|
||||||
|
"contact:delete": { title: "Delete contacts", risk: "high" },
|
||||||
|
"deal:read": { title: "View deals" },
|
||||||
|
"deal:write": { title: "Create and edit deals" },
|
||||||
|
"admin:access": { title: "Manage CRM access", risk: "high" },
|
||||||
|
},
|
||||||
|
roles: {
|
||||||
|
viewer: ["crm:dashboard", "contact:read", "deal:read"],
|
||||||
|
"sales-rep": ["role:viewer", "contact:write", "deal:write"],
|
||||||
|
manager: ["role:sales-rep", "contact:delete"],
|
||||||
|
admin: ["role:manager", "admin:access"],
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
-- +up
|
||||||
|
CREATE TABLE IF NOT EXISTS crm_contacts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
owner_id TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
company TEXT NOT NULL DEFAULT '',
|
||||||
|
phone TEXT NOT NULL DEFAULT '',
|
||||||
|
status TEXT NOT NULL DEFAULT 'lead' CHECK (status IN ('lead', 'customer', 'inactive')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS crm_contacts_owner_idx ON crm_contacts(owner_id);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS crm_contacts_owner_email_uq ON crm_contacts(owner_id, email);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS crm_deals (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
owner_id TEXT NOT NULL,
|
||||||
|
contact_id INTEGER REFERENCES crm_contacts(id) ON DELETE SET NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
value_cents INTEGER NOT NULL DEFAULT 0 CHECK (value_cents >= 0),
|
||||||
|
stage TEXT NOT NULL DEFAULT 'qualified' CHECK (stage IN ('qualified', 'proposal', 'won', 'lost')),
|
||||||
|
close_date TEXT,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS crm_deals_owner_idx ON crm_deals(owner_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS crm_deals_contact_idx ON crm_deals(contact_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS crm_activities (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
actor_id TEXT NOT NULL,
|
||||||
|
entity_type TEXT NOT NULL CHECK (entity_type IN ('contact', 'deal', 'account')),
|
||||||
|
entity_id TEXT NOT NULL,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
details_json TEXT NOT NULL DEFAULT '{}',
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS crm_activities_entity_idx ON crm_activities(entity_type, entity_id);
|
||||||
|
|
||||||
|
-- Database-backed authorization assignments.
|
||||||
|
CREATE TABLE IF NOT EXISTS _wrn_authz_assignment (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
subject_id VARCHAR(255) NOT NULL,
|
||||||
|
scope VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
role VARCHAR(255) NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT _wrn_authz_assignment_unique UNIQUE (subject_id, scope, role)
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS _wrn_authz_grant (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
subject_id VARCHAR(255) NOT NULL,
|
||||||
|
scope VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
permission VARCHAR(255) NOT NULL,
|
||||||
|
effect VARCHAR(16) NOT NULL CHECK (effect IN ('allow', 'deny')),
|
||||||
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT _wrn_authz_grant_unique UNIQUE (subject_id, scope, permission)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- +down
|
||||||
|
DROP TABLE IF EXISTS _wrn_authz_grant;
|
||||||
|
DROP TABLE IF EXISTS _wrn_authz_assignment;
|
||||||
|
DROP TABLE IF EXISTS crm_activities;
|
||||||
|
DROP TABLE IF EXISTS crm_deals;
|
||||||
|
DROP TABLE IF EXISTS crm_contacts;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { Db } from "@wrnexus/db";
|
||||||
|
|
||||||
|
export default async function seed(db: Db): Promise<void> {
|
||||||
|
const owner = "demo-owner";
|
||||||
|
await db.exec("DELETE FROM crm_activities");
|
||||||
|
await db.exec("DELETE FROM crm_deals");
|
||||||
|
await db.exec("DELETE FROM crm_contacts");
|
||||||
|
await db.exec(
|
||||||
|
"INSERT INTO crm_contacts (owner_id,name,email,company,phone,status) VALUES (?,?,?,?,?,?)",
|
||||||
|
[owner, "Ada Lovelace", "ada@example.test", "Analytical Engines", "+1 555 0101", "customer"],
|
||||||
|
);
|
||||||
|
await db.exec(
|
||||||
|
"INSERT INTO crm_contacts (owner_id,name,email,company,phone,status) VALUES (?,?,?,?,?,?)",
|
||||||
|
[owner, "Grace Hopper", "grace@example.test", "Compiler Labs", "+1 555 0102", "lead"],
|
||||||
|
);
|
||||||
|
const contact = await db.one<{ id: number }>("SELECT id FROM crm_contacts WHERE email = ?", [
|
||||||
|
"ada@example.test",
|
||||||
|
]);
|
||||||
|
await db.exec(
|
||||||
|
"INSERT INTO crm_deals (owner_id,contact_id,title,value_cents,stage,close_date) VALUES (?,?,?,?,?,?)",
|
||||||
|
[owner, contact?.id ?? null, "Enterprise rollout", 12500000, "proposal", "2026-12-15"],
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { createAuthEngine, SqlAuthStore, type AuthStore } from "@wrnexus/auth";
|
||||||
|
import { getDb } from "@wrnexus/db";
|
||||||
|
import { dbPermissionStore } from "@wrnexus/authz/db";
|
||||||
|
import { ensureCrmWorkspace } from "./workspace.ts";
|
||||||
|
|
||||||
|
// Config is imported before the runtime opens its configured database. This
|
||||||
|
// forwarding store resolves getDb() only when an auth operation actually runs.
|
||||||
|
const store = new Proxy({} as AuthStore, {
|
||||||
|
get(_target, property) {
|
||||||
|
const value = Reflect.get(new SqlAuthStore(getDb()), property);
|
||||||
|
return typeof value === "function" ? value.bind(new SqlAuthStore(getDb())) : value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const auth = createAuthEngine({
|
||||||
|
store,
|
||||||
|
secret: process.env.AUTH_SECRET ?? "northstar-crm-development-secret-change-me",
|
||||||
|
issuer: "Northstar CRM",
|
||||||
|
onSignedIn(ctx, returnTo) {
|
||||||
|
const destination =
|
||||||
|
returnTo?.startsWith("/") && !returnTo.startsWith("//") ? returnTo : "/dashboard";
|
||||||
|
return Response.redirect(new URL(destination, ctx.url), 303);
|
||||||
|
},
|
||||||
|
onSignedOut(ctx) {
|
||||||
|
return Response.redirect(new URL("/", ctx.url), 303);
|
||||||
|
},
|
||||||
|
async onSuccessfulSignUp(_ctx, user) {
|
||||||
|
await dbPermissionStore(getDb()).assignRole(user.id, "sales-rep");
|
||||||
|
await ensureCrmWorkspace(user.id);
|
||||||
|
return { autoSignIn: true, redirectTo: "/dashboard" };
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { getDb } from "@wrnexus/db";
|
||||||
|
|
||||||
|
/** Give every real CRM user a useful workspace on first access. */
|
||||||
|
export async function ensureCrmWorkspace(ownerId: string): Promise<void> {
|
||||||
|
if (!ownerId) return;
|
||||||
|
const db = getDb();
|
||||||
|
await db.exec(
|
||||||
|
"INSERT OR IGNORE INTO crm_contacts (owner_id,name,email,company,phone,status) VALUES (?,?,?,?,?,?)",
|
||||||
|
[ownerId, "Ada Lovelace", "ada@example.test", "Analytical Engines", "+1 555 0101", "customer"],
|
||||||
|
);
|
||||||
|
await db.exec(
|
||||||
|
"INSERT OR IGNORE INTO crm_contacts (owner_id,name,email,company,phone,status) VALUES (?,?,?,?,?,?)",
|
||||||
|
[ownerId, "Grace Hopper", "grace@example.test", "Compiler Labs", "+1 555 0102", "lead"],
|
||||||
|
);
|
||||||
|
const existingDeal = await db.one<{ count: number }>(
|
||||||
|
"SELECT COUNT(*) count FROM crm_deals WHERE owner_id = ?",
|
||||||
|
[ownerId],
|
||||||
|
);
|
||||||
|
if (Number(existingDeal?.count ?? 0) === 0) {
|
||||||
|
const contact = await db.one<{ id: number }>(
|
||||||
|
"SELECT id FROM crm_contacts WHERE owner_id = ? AND email = ?",
|
||||||
|
[ownerId, "ada@example.test"],
|
||||||
|
);
|
||||||
|
await db.exec(
|
||||||
|
"INSERT INTO crm_deals (owner_id,contact_id,title,value_cents,stage,close_date) VALUES (?,?,?,?,?,?)",
|
||||||
|
[ownerId, contact?.id ?? null, "Enterprise rollout", 12500000, "proposal", "2026-12-15"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { authzMiddleware, getAuthzCatalog } from "@wrnexus/authz";
|
||||||
|
import { getDb } from "@wrnexus/db";
|
||||||
|
import { dbPermissionStore } from "@wrnexus/authz/db";
|
||||||
|
import type { Middleware } from "@wrnexus/core";
|
||||||
|
|
||||||
|
// Production imports middleware before it opens configured databases. Resolve
|
||||||
|
// the SQL-backed store on the first request, after runtime initialization.
|
||||||
|
const middleware: Middleware = (ctx, next) =>
|
||||||
|
authzMiddleware({ catalog: getAuthzCatalog(), store: dbPermissionStore(getDb()) })(ctx, next);
|
||||||
|
|
||||||
|
export default middleware;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { requireAuth } from "@wrnexus/auth";
|
||||||
|
import { can } from "@wrnexus/authz";
|
||||||
|
import type { Middleware } from "@wrnexus/core";
|
||||||
|
|
||||||
|
const guard = requireAuth({ loginPath: "/login" });
|
||||||
|
const protectedPrefixes = ["/dashboard", "/contacts", "/deals", "/admin"];
|
||||||
|
|
||||||
|
const middleware: Middleware = (ctx, next) => {
|
||||||
|
if (!protectedPrefixes.some((prefix) => ctx.url.pathname.startsWith(prefix))) return next();
|
||||||
|
return guard(ctx, async () => {
|
||||||
|
if (ctx.url.pathname.startsWith("/admin") && !(await can(ctx, "admin:access"))) {
|
||||||
|
return Response.redirect(new URL("/forbidden", ctx.url), 303);
|
||||||
|
}
|
||||||
|
return next();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default middleware;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
page Admin {
|
||||||
|
state user = ctx.user
|
||||||
|
seo { title = "Administration" }
|
||||||
|
view { <main class="crm-shell"><aside class="crm-sidebar"><a class="crm-brand" href="/"><span class="crm-brand-mark">N</span>Northstar CRM</a><nav class="crm-menu"><a href="/dashboard">Overview</a><a href="/contacts">Contacts</a><a href="/deals">Deals</a><a href="/admin" aria-current="page">Administration</a></nav></aside><section class="crm-content"><header class="crm-topbar"><div><h1>Access administration</h1><p>Manage roles and protect high-risk operations.</p></div></header><section class="crm-panel"><span class="crm-eyebrow">Role-based access</span><h2>Signed in as {user.displayName || user.username}</h2><p class="crm-hero-copy">This route requires the <strong>admin:access</strong> permission. Assign manager and administrator roles through the database-backed authorization store.</p></section></section></main> }
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
page Contacts {
|
||||||
|
apis { listContacts GET /api/contacts { response { return data.contacts } } }
|
||||||
|
load server contacts { return await api.listContacts() }
|
||||||
|
seo { title = "Contacts" }
|
||||||
|
view { <main class="crm-shell"><aside class="crm-sidebar"><a class="crm-brand" href="/"><span class="crm-brand-mark">N</span>Northstar CRM</a><nav class="crm-menu"><a href="/dashboard">Overview</a><a href="/contacts" aria-current="page">Contacts</a><a href="/deals">Deals</a><a href="/admin">Administration</a></nav></aside><section class="crm-content"><header class="crm-topbar"><div><h1>Contacts</h1><p>Every relationship, scoped securely to its owner.</p></div><a class="crm-button" href="mailto:sales@example.test">New contact</a></header><section class="crm-panel"><span class="crm-eyebrow">Customer directory</span>{#if contacts.length}<ul class="crm-list">{#each contacts as contact}<li><strong>{contact.name}</strong><span>{contact.company || contact.email}</span><span class="crm-pill">{contact.status}</span></li>{/each}</ul>{/if}{#if !contacts.length}<div class="crm-empty"><h2>No contacts yet</h2><p>Your first customer relationship will appear here.</p></div>{/if}</section></section></main> }
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
page Dashboard {
|
||||||
|
state user = ctx.user
|
||||||
|
state dashboardMetrics = ctx.metrics
|
||||||
|
apis { dashboard GET /api/dashboard { response { return data.metrics } } }
|
||||||
|
load server metrics { return await api.dashboard() }
|
||||||
|
seo { title = "Dashboard" }
|
||||||
|
view { <main class="crm-shell"><aside class="crm-sidebar"><a class="crm-brand" href="/"><span class="crm-brand-mark">N</span>Northstar CRM</a><nav class="crm-menu"><a href="/dashboard" aria-current="page">Overview</a><a href="/contacts">Contacts</a><a href="/deals">Deals</a><a href="/admin">Administration</a></nav><form class="crm-sidebar-footer" method="post" action="/api/auth/logout" data-schema="auth-empty"><button class="crm-button crm-button--ghost" type="submit">Log out</button></form></aside><section class="crm-content"><header class="crm-topbar"><div><h1>Good to see you, {user.displayName || user.username}</h1><p>Live figures from your private SQLite workspace.</p></div><a class="crm-button" href="/contacts">View contacts</a></header><div class="crm-stat-grid"><article class="crm-stat"><span>Pipeline value</span><strong>{dashboardMetrics.pipelineValue}</strong></article><article class="crm-stat"><span>Open opportunities</span><strong>{dashboardMetrics.openOpportunities}</strong></article><article class="crm-stat"><span>Total contacts</span><strong>{dashboardMetrics.contacts}</strong></article></div><section class="crm-panel"><span class="crm-eyebrow">Server rendered</span><h2>One secure data path</h2><p class="crm-hero-copy">This page was rendered from authenticated API results on the server. No browser prefetch or client data request is needed to show these metrics.</p></section></section></main> }
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
page Deals {
|
||||||
|
apis { listDeals GET /api/deals { response { return data.deals } } }
|
||||||
|
load server deals { return await api.listDeals() }
|
||||||
|
seo { title = "Deals" }
|
||||||
|
view { <main class="crm-shell"><aside class="crm-sidebar"><a class="crm-brand" href="/"><span class="crm-brand-mark">N</span>Northstar CRM</a><nav class="crm-menu"><a href="/dashboard">Overview</a><a href="/contacts">Contacts</a><a href="/deals" aria-current="page">Deals</a><a href="/admin">Administration</a></nav></aside><section class="crm-content"><header class="crm-topbar"><div><h1>Sales pipeline</h1><p>Focus on the opportunities most likely to move.</p></div><a class="crm-button" href="/contacts">New deal</a></header><section class="crm-panel"><span class="crm-eyebrow">Open opportunities</span>{#if deals.length}<ul class="crm-list">{#each deals as deal}<li><strong>{deal.title}</strong><span>{deal.contact_name || "Unassigned contact"}</span><span class="crm-pill">{deal.stage}</span></li>{/each}</ul>{/if}{#if !deals.length}<div class="crm-empty"><h2>No open deals</h2><p>Create a contact and turn the relationship into an opportunity.</p></div>{/if}</section></section></main> }
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
page Forbidden {
|
||||||
|
seo { title = "Access denied" }
|
||||||
|
view {
|
||||||
|
<main class="crm-status-page"><section class="crm-status-card"><span class="crm-status-icon" aria-hidden="true">!</span><span class="crm-eyebrow">Permission required</span><h1>You do not have access to administration.</h1><p>Your account is working correctly, but an administrator role is required for this page.</p><div class="crm-actions"><a class="crm-button" href="/dashboard">Back to dashboard</a><a class="crm-button crm-button--ghost" href="/contacts">View contacts</a></div></section></main>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
page Home {
|
||||||
|
seo { title = "CRM that keeps sales moving" }
|
||||||
|
view {
|
||||||
|
<main
|
||||||
|
class="crm-public"
|
||||||
|
>
|
||||||
|
<nav
|
||||||
|
class="crm-nav"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="crm-brand"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="crm-brand-mark"
|
||||||
|
>
|
||||||
|
N</span>Northstar CRM</a><div class="crm-nav-links"><a href="/pricing">Pricing</a><a href="/login">Log in</a><a class="crm-button" href="/signup">Start free</a></div>
|
||||||
|
</nav>
|
||||||
|
<section
|
||||||
|
class="crm-hero"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
class="crm-eyebrow"
|
||||||
|
>
|
||||||
|
Customer intelligence, simplified</span><h1>Know every customer. Close every opportunity.</h1><p class="crm-hero-copy">Bring contacts, conversations, and pipeline into one calm workspace built for teams that value clarity.</p><div class="crm-actions"><a class="crm-button" href="/signup">Create your workspace</a><a class="crm-button crm-button--ghost" href="/login">View the CRM</a></div></div><div class="crm-preview"><div class="crm-preview-bar"><span></span><span></span><span></span></div><div class="crm-preview-grid"><article class="crm-preview-card"><small>Pipeline value</small><strong>$125k</strong><small>+18% this month</small></article><article class="crm-preview-card"><small>Active contacts</small><strong>248</strong><small>32 need follow-up</small></article><article class="crm-preview-card"><small>Win rate</small><strong>42%</strong><small>Across qualified deals</small></article><article class="crm-preview-card"><small>Next action</small><strong>8 today</strong><small>Stay ahead of every promise</small></article></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import AuthSplitLayout from "@wrnexus/ui/components/AuthSplitLayout.wrn"
|
||||||
|
|
||||||
|
page Login {
|
||||||
|
seo { title = "Log in" }
|
||||||
|
view {
|
||||||
|
<AuthSplitLayout brand="Northstar CRM" eyebrow="Customer intelligence" title="Turn every conversation into momentum." description="Sign in to a focused workspace for contacts, pipeline, and activity." features='[{"label":"One customer timeline","description":"Every relationship and opportunity in context."},{"label":"Secure by default","description":"SQL sessions and role-based authorization."},{"label":"Built for focus","description":"A calm workspace without sales-tool clutter."}]'>
|
||||||
|
<div data-slot="form"><SignIn action="/api/auth/login" returnTo="/dashboard" signUpHref="/signup" forgotHref="/login" showPasskey="false" class="border-0 shadow-none" /></div>
|
||||||
|
</AuthSplitLayout>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
page Pricing {
|
||||||
|
seo { title = "Pricing" }
|
||||||
|
view { <main class="crm-public"><nav class="crm-nav"><a class="crm-brand" href="/"><span class="crm-brand-mark">N</span>Northstar CRM</a><div class="crm-nav-links"><a href="/login">Log in</a><a class="crm-button" href="/signup">Start free</a></div></nav><section class="crm-hero"><div><span class="crm-eyebrow">Simple pricing</span><h1>Start free. Scale when your team does.</h1><p class="crm-hero-copy">Everything needed to evaluate a secure, full-stack CRM locally. Move to Team when you are ready to collaborate.</p></div><div class="crm-preview"><article class="crm-preview-card"><small>Starter</small><strong>$0</strong><p>SQLite workspace, CRM flows, authentication and permissions.</p><a class="crm-button" href="/signup">Start building</a></article><article class="crm-preview-card"><small>Team</small><strong>$29</strong><p>Per user/month with shared pipeline and administration.</p></article></div></section></main> }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import AuthSplitLayout from "@wrnexus/ui/components/AuthSplitLayout.wrn"
|
||||||
|
|
||||||
|
page Signup {
|
||||||
|
seo { title = "Create account" }
|
||||||
|
view {
|
||||||
|
<AuthSplitLayout brand="Northstar CRM" eyebrow="Start in minutes" title="Build relationships, not spreadsheets." description="Create your secure sales workspace and begin with a complete customer view." features='[{"label":"Free local workspace","description":"Explore the complete CRM flow with SQLite."},{"label":"Flexible permissions","description":"Viewer, sales, manager, and admin roles."},{"label":"Your data stays yours","description":"Portable SQL data and explicit migrations."}]'>
|
||||||
|
<div data-slot="form"><SignUp redirect="/dashboard" signInHref="/login" showPhone="false" showUsername="false" requireConsent="false" class="border-0 shadow-none" /></div>
|
||||||
|
</AuthSplitLayout>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
// AUTO-GENERATED by `wrnexus dev` - do not edit.
|
||||||
|
// Typed routes support required, optional, and catch-all parameters.
|
||||||
|
|
||||||
|
export interface Routes {
|
||||||
|
"/": Record<string, never>;
|
||||||
|
"/admin": Record<string, never>;
|
||||||
|
"/contacts": Record<string, never>;
|
||||||
|
"/dashboard": Record<string, never>;
|
||||||
|
"/deals": Record<string, never>;
|
||||||
|
"/forbidden": Record<string, never>;
|
||||||
|
"/login": Record<string, never>;
|
||||||
|
"/pricing": Record<string, never>;
|
||||||
|
"/signup": Record<string, never>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RouteNames {
|
||||||
|
"index": "/";
|
||||||
|
"admin": "/admin";
|
||||||
|
"contacts": "/contacts";
|
||||||
|
"dashboard": "/dashboard";
|
||||||
|
"deals": "/deals";
|
||||||
|
"forbidden": "/forbidden";
|
||||||
|
"login": "/login";
|
||||||
|
"pricing": "/pricing";
|
||||||
|
"signup": "/signup";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RouteQueries {
|
||||||
|
[path: string]: Record<string, string | number | boolean | null | undefined>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RoutePath = keyof Routes;
|
||||||
|
export type RouteName = keyof RouteNames;
|
||||||
|
export type RouteQuery<P extends RoutePath> = P extends keyof RouteQueries
|
||||||
|
? RouteQueries[P]
|
||||||
|
: Record<string, string | number | boolean | null | undefined>;
|
||||||
|
type RouteValue = string | readonly string[] | undefined;
|
||||||
|
|
||||||
|
function encodeRouteValue(value: RouteValue, catchAll: boolean): string {
|
||||||
|
if (value === undefined) return "";
|
||||||
|
const values = Array.isArray(value) ? value : catchAll ? String(value).split("/") : [String(value)];
|
||||||
|
return values.map((part) => encodeURIComponent(part)).join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHref(path: string, params: Record<string, RouteValue> = {}): string {
|
||||||
|
const output: string[] = [];
|
||||||
|
for (const segment of path.split("/").filter(Boolean)) {
|
||||||
|
let name: string | undefined;
|
||||||
|
let optional = false;
|
||||||
|
let catchAll = false;
|
||||||
|
if (segment.startsWith("[[") && segment.endsWith("]]")) {
|
||||||
|
optional = true;
|
||||||
|
name = segment.slice(2, -2);
|
||||||
|
} else if (segment.startsWith("[") && segment.endsWith("]")) {
|
||||||
|
name = segment.slice(1, -1);
|
||||||
|
if (name.endsWith("?")) {
|
||||||
|
optional = true;
|
||||||
|
name = name.slice(0, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!name) {
|
||||||
|
output.push(segment);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (name.startsWith("...")) {
|
||||||
|
catchAll = true;
|
||||||
|
name = name.slice(3);
|
||||||
|
}
|
||||||
|
const value = params[name];
|
||||||
|
if (value === undefined && optional) continue;
|
||||||
|
if (value === undefined) throw new Error(`WRN-ROUTE-MISSING-PARAM: Missing route parameter '${name}'.`);
|
||||||
|
output.push(encodeRouteValue(value, catchAll));
|
||||||
|
}
|
||||||
|
return "/" + output.filter(Boolean).join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function href<P extends RoutePath>(
|
||||||
|
path: P,
|
||||||
|
...args: keyof Routes[P] extends never
|
||||||
|
? []
|
||||||
|
: Record<string, never> extends Routes[P]
|
||||||
|
? [params?: Routes[P]]
|
||||||
|
: [params: Routes[P]]
|
||||||
|
): string {
|
||||||
|
const params = (args[0] ?? {}) as Record<string, RouteValue>;
|
||||||
|
return buildHref(String(path), params);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function route<N extends RouteName>(
|
||||||
|
name: N,
|
||||||
|
...args: keyof Routes[RouteNames[N]] extends never
|
||||||
|
? [params?: Routes[RouteNames[N]], query?: RouteQuery<RouteNames[N]>]
|
||||||
|
: Record<string, never> extends Routes[RouteNames[N]]
|
||||||
|
? [params?: Routes[RouteNames[N]], query?: RouteQuery<RouteNames[N]>]
|
||||||
|
: [params: Routes[RouteNames[N]], query?: RouteQuery<RouteNames[N]>]
|
||||||
|
): string {
|
||||||
|
const paths: Record<RouteName, RoutePath> = {
|
||||||
|
"index": "/",
|
||||||
|
"admin": "/admin",
|
||||||
|
"contacts": "/contacts",
|
||||||
|
"dashboard": "/dashboard",
|
||||||
|
"deals": "/deals",
|
||||||
|
"forbidden": "/forbidden",
|
||||||
|
"login": "/login",
|
||||||
|
"pricing": "/pricing",
|
||||||
|
"signup": "/signup"
|
||||||
|
} as Record<RouteName, RoutePath>;
|
||||||
|
const output = buildHref(paths[name], (args[0] ?? {}) as Record<string, RouteValue>);
|
||||||
|
const query = args[1];
|
||||||
|
if (!query) return output;
|
||||||
|
const search = new URLSearchParams();
|
||||||
|
for (const [key, value] of Object.entries(query))
|
||||||
|
if (value !== undefined && value !== null) search.set(key, String(value));
|
||||||
|
const text = search.toString();
|
||||||
|
return text ? `${output}?${text}` : output;
|
||||||
|
}
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@plugin "@iconify/tailwind4";
|
||||||
|
@source "../**/*.wrn";
|
||||||
|
@source "../../../packages/auth/components/*.wrn";
|
||||||
|
@source "../../../packages/ui/components/*.wrn";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
min-width: 320px;
|
||||||
|
background: var(--wrn-color-bg);
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
color: var(--wrn-color-text);
|
||||||
|
background: var(--wrn-color-bg);
|
||||||
|
font-family: Inter, "Plus Jakarta Sans", ui-sans-serif, system-ui, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-public {
|
||||||
|
min-height: 100dvh;
|
||||||
|
overflow: hidden;
|
||||||
|
background:
|
||||||
|
radial-gradient(
|
||||||
|
circle at 80% 10%,
|
||||||
|
color-mix(in srgb, var(--wrn-color-primary) 15%, transparent),
|
||||||
|
transparent 28%
|
||||||
|
),
|
||||||
|
var(--wrn-color-bg);
|
||||||
|
}
|
||||||
|
.crm-nav {
|
||||||
|
width: min(1180px, calc(100% - 2rem));
|
||||||
|
margin: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1.25rem 0;
|
||||||
|
}
|
||||||
|
.crm-brand {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.7rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
}
|
||||||
|
.crm-brand-mark {
|
||||||
|
display: grid;
|
||||||
|
width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
color: white;
|
||||||
|
background: linear-gradient(135deg, var(--wrn-color-primary), #7c3aed);
|
||||||
|
box-shadow: 0 10px 24px color-mix(in srgb, var(--wrn-color-primary) 28%, transparent);
|
||||||
|
}
|
||||||
|
.crm-nav-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
color: var(--wrn-color-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.crm-button {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 2.75rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 1.1rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
color: white;
|
||||||
|
background: var(--wrn-color-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
box-shadow: 0 10px 24px color-mix(in srgb, var(--wrn-color-primary) 22%, transparent);
|
||||||
|
}
|
||||||
|
.crm-button--ghost {
|
||||||
|
color: var(--wrn-color-text);
|
||||||
|
background: var(--wrn-color-surface);
|
||||||
|
border: 1px solid var(--wrn-color-border);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.crm-hero {
|
||||||
|
width: min(1180px, calc(100% - 2rem));
|
||||||
|
margin: auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.05fr 0.95fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4rem;
|
||||||
|
padding: clamp(4rem, 9vw, 8rem) 0;
|
||||||
|
}
|
||||||
|
.crm-eyebrow {
|
||||||
|
color: var(--wrn-color-primary);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.13em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.crm-hero h1 {
|
||||||
|
max-width: 12ch;
|
||||||
|
margin: 1rem 0;
|
||||||
|
font-size: clamp(3rem, 7vw, 5.5rem);
|
||||||
|
line-height: 0.98;
|
||||||
|
letter-spacing: -0.065em;
|
||||||
|
}
|
||||||
|
.crm-hero-copy {
|
||||||
|
max-width: 38rem;
|
||||||
|
color: var(--wrn-color-muted);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.75;
|
||||||
|
}
|
||||||
|
.crm-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.8rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
.crm-preview {
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--wrn-color-border);
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
background: color-mix(in srgb, var(--wrn-color-surface) 88%, transparent);
|
||||||
|
box-shadow: 0 28px 80px rgba(15, 23, 42, 0.15);
|
||||||
|
transform: rotate(1.5deg);
|
||||||
|
}
|
||||||
|
.crm-preview-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.4rem 0.25rem 1rem;
|
||||||
|
}
|
||||||
|
.crm-preview-bar span {
|
||||||
|
width: 0.6rem;
|
||||||
|
height: 0.6rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--wrn-color-border);
|
||||||
|
}
|
||||||
|
.crm-preview-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
.crm-preview-card {
|
||||||
|
min-height: 8rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: var(--wrn-color-surface-2);
|
||||||
|
}
|
||||||
|
.crm-preview-card strong {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.7rem;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
.crm-preview-card small {
|
||||||
|
color: var(--wrn-color-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.crm-shell {
|
||||||
|
min-height: 100dvh;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 16rem 1fr;
|
||||||
|
background: var(--wrn-color-bg);
|
||||||
|
}
|
||||||
|
.crm-sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
height: 100dvh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1.35rem;
|
||||||
|
border-right: 1px solid var(--wrn-color-border);
|
||||||
|
background: var(--wrn-color-surface);
|
||||||
|
}
|
||||||
|
.crm-menu {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.35rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
.crm-menu a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem 0.85rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
color: var(--wrn-color-muted);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
.crm-menu a:hover,
|
||||||
|
.crm-menu a[aria-current="page"] {
|
||||||
|
color: var(--wrn-color-primary);
|
||||||
|
background: color-mix(in srgb, var(--wrn-color-primary) 10%, transparent);
|
||||||
|
}
|
||||||
|
.crm-sidebar-footer {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
.crm-content {
|
||||||
|
min-width: 0;
|
||||||
|
padding: clamp(1.25rem, 4vw, 3rem);
|
||||||
|
}
|
||||||
|
.crm-topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.crm-topbar h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: clamp(1.75rem, 4vw, 2.4rem);
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
}
|
||||||
|
.crm-topbar p {
|
||||||
|
margin: 0.4rem 0 0;
|
||||||
|
color: var(--wrn-color-muted);
|
||||||
|
}
|
||||||
|
.crm-panel {
|
||||||
|
padding: 1.25rem;
|
||||||
|
border: 1px solid var(--wrn-color-border);
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: var(--wrn-color-surface);
|
||||||
|
box-shadow: var(--wrn-shadow-1);
|
||||||
|
}
|
||||||
|
.crm-stat-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.crm-stat {
|
||||||
|
padding: 1.25rem;
|
||||||
|
border: 1px solid var(--wrn-color-border);
|
||||||
|
border-radius: 1rem;
|
||||||
|
background: var(--wrn-color-surface);
|
||||||
|
}
|
||||||
|
.crm-stat span {
|
||||||
|
color: var(--wrn-color-muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
.crm-stat strong {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
font-size: 2rem;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
}
|
||||||
|
.crm-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.65rem;
|
||||||
|
padding: 0;
|
||||||
|
margin: 1rem 0 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
.crm-list li {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(10rem, 1fr) minmax(8rem, 0.7fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--wrn-color-border);
|
||||||
|
border-radius: 0.8rem;
|
||||||
|
background: var(--wrn-color-surface-2);
|
||||||
|
}
|
||||||
|
.crm-pill {
|
||||||
|
justify-self: end;
|
||||||
|
padding: 0.3rem 0.65rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--wrn-color-primary);
|
||||||
|
background: color-mix(in srgb, var(--wrn-color-primary) 10%, transparent);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 750;
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
.crm-empty {
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--wrn-color-muted);
|
||||||
|
}
|
||||||
|
.crm-empty h2 {
|
||||||
|
margin-bottom: 0.35rem;
|
||||||
|
color: var(--wrn-color-text);
|
||||||
|
}
|
||||||
|
.crm-status-page {
|
||||||
|
min-height: 100dvh;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle at 50% 0%,
|
||||||
|
color-mix(in srgb, var(--wrn-color-primary) 12%, transparent),
|
||||||
|
transparent 45%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.crm-status-card {
|
||||||
|
width: min(34rem, 100%);
|
||||||
|
padding: clamp(1.5rem, 5vw, 3rem);
|
||||||
|
border: 1px solid var(--wrn-color-border);
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
background: var(--wrn-color-surface);
|
||||||
|
box-shadow: var(--wrn-shadow-2);
|
||||||
|
}
|
||||||
|
.crm-status-card h1 {
|
||||||
|
margin: 0.9rem 0 0.7rem;
|
||||||
|
font-size: clamp(1.8rem, 5vw, 2.5rem);
|
||||||
|
line-height: 1.08;
|
||||||
|
letter-spacing: -0.045em;
|
||||||
|
}
|
||||||
|
.crm-status-card > p {
|
||||||
|
color: var(--wrn-color-muted);
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
.crm-status-icon {
|
||||||
|
display: grid;
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 1rem;
|
||||||
|
color: #b45309;
|
||||||
|
background: #fef3c7;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 820px) {
|
||||||
|
.crm-nav-links > a:not(.crm-button) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.crm-hero {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding-top: 3rem;
|
||||||
|
}
|
||||||
|
.crm-preview {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
.crm-shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.crm-sidebar {
|
||||||
|
position: static;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.crm-menu {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.crm-sidebar-footer {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.crm-stat-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
.crm-hero h1 {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
.crm-menu {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
.crm-list li {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.crm-pill {
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// AUTO-GENERATED by `wrnexus generate types` - do not edit.
|
||||||
|
//
|
||||||
|
// Type-only assertions for sectioned `api` blocks. Kept as a real .ts file (not
|
||||||
|
// wrnexus.generated.d.ts) because `skipLibCheck` exempts .d.ts contents from being
|
||||||
|
// checked; this file is compiled and checked normally by the project's own tsc.
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// AUTO-GENERATED by `wrnexus generate types` - do not edit.
|
||||||
|
declare namespace WRNexusGenerated {
|
||||||
|
type ApiContract<T> = T extends import("@wrnexus/core").DefinedEndpoint<infer I, infer O>
|
||||||
|
? { input: I; output: O }
|
||||||
|
: T extends (...args: infer A) => infer R
|
||||||
|
? { input: A extends [any, infer I, ...any[]] ? I : unknown; output: Awaited<R> }
|
||||||
|
: { input: unknown; output: unknown };
|
||||||
|
type MiddlewareContext<T> = T extends (ctx: infer C, ...args: any[]) => any ? C : never;
|
||||||
|
type QueryContract<T> = T extends (db: any, args: infer A, ...rest: any[]) => infer R
|
||||||
|
? { args: A; result: Awaited<R> }
|
||||||
|
: T extends (db: any, ...rest: any[]) => infer R
|
||||||
|
? { args: Record<string, never>; result: Awaited<R> }
|
||||||
|
: never;
|
||||||
|
type RealtimeMessage<T> = T extends import("@wrnexus/core").RoomDefinition<any, infer M> ? M : unknown;
|
||||||
|
type QueuePayload<T> = T extends import("@wrnexus/queue").JobDefinition<infer I> ? I : unknown;
|
||||||
|
type RouteName = "admin" | "contacts" | "dashboard" | "deals" | "forbidden" | "index" | "login" | "pricing" | "signup";
|
||||||
|
type ApiRoute = "/api/contacts" | "/api/dashboard" | "/api/deals";
|
||||||
|
type RealtimeRoute = never;
|
||||||
|
type EnvironmentKey = never;
|
||||||
|
type TranslationKey = never;
|
||||||
|
type QueueName = never;
|
||||||
|
type CacheKey = never;
|
||||||
|
type Components = Record<string, never>;
|
||||||
|
interface ApiContracts {
|
||||||
|
"/api/dashboard": { GET: ApiContract<typeof import("../api/dashboard.ts")["GET"]> };
|
||||||
|
"/api/contacts": { GET: ApiContract<typeof import("../api/contacts.ts")["GET"]>; POST: ApiContract<typeof import("../api/contacts.ts")["POST"]> };
|
||||||
|
"/api/deals": { GET: ApiContract<typeof import("../api/deals.ts")["GET"]> };
|
||||||
|
}
|
||||||
|
interface MiddlewareContexts {
|
||||||
|
"authz": MiddlewareContext<(typeof import("../middleware/authz.ts"))["default"]>;
|
||||||
|
"protected": MiddlewareContext<(typeof import("../middleware/protected.ts"))["default"]>;
|
||||||
|
}
|
||||||
|
type DatabaseQueries = Record<string, never>;
|
||||||
|
type RealtimeMessages = Record<string, never>;
|
||||||
|
type QueuePayloads = Record<string, never>;
|
||||||
|
type ApplicationConfig = (typeof import("../../wrnexus.config.ts"))["default"];
|
||||||
|
type AssertAssignable<Actual, Expected> = unknown extends Expected
|
||||||
|
? true
|
||||||
|
: [Actual] extends [Expected]
|
||||||
|
? [Exclude<keyof Actual, keyof Expected>] extends [never]
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
: false;
|
||||||
|
type __wrn_expect_true<T extends true> = T;
|
||||||
|
type ApiInput<P extends ApiRoute, M> = ApiContracts[P][M]["input"];
|
||||||
|
type ApiOutput<P extends ApiRoute, M> = ApiContracts[P][M]["output"];
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
// AUTO-GENERATED plugin type aggregation - do not edit.
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "wrnexus-crm-example",
|
||||||
|
"version": "0.8.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "bun run ../../packages/cli/src/index.ts dev .",
|
||||||
|
"build": "bun run ../../packages/cli/src/index.ts build .",
|
||||||
|
"db:migrate": "bun run ../../packages/cli/src/index.ts db migrate .",
|
||||||
|
"db:seed": "bun run ../../packages/cli/src/index.ts db seed .",
|
||||||
|
"test": "bun test test",
|
||||||
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
||||||
|
"check": "bun run typecheck && bun run test && bun run build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@wrnexus/auth": "workspace:*",
|
||||||
|
"@wrnexus/authz": "workspace:*",
|
||||||
|
"@wrnexus/core": "workspace:*",
|
||||||
|
"@wrnexus/db": "workspace:*",
|
||||||
|
"@wrnexus/styles": "workspace:*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@iconify-json/lucide": "^1.2.123",
|
||||||
|
"@iconify/tailwind4": "^1.2.3",
|
||||||
|
"@tailwindcss/cli": "^4.3.3",
|
||||||
|
"@types/bun": "^1.3.14",
|
||||||
|
"tailwindcss": "^4.3.3",
|
||||||
|
"typescript": "^6.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { createDb, loadMigrations, migrate } from "@wrnexus/db";
|
||||||
|
import { sqlite } from "@wrnexus/db/sqlite";
|
||||||
|
import seed from "../app/db/seed.ts";
|
||||||
|
|
||||||
|
const root = join(import.meta.dir, "..");
|
||||||
|
const source = (path: string) => readFileSync(join(root, path), "utf8");
|
||||||
|
|
||||||
|
test("the CRM migration applies to SQLite, is idempotent, and the seed is repeatable", async () => {
|
||||||
|
const db = createDb(sqlite());
|
||||||
|
const migrations = join(root, "app", "db", "migrations");
|
||||||
|
|
||||||
|
expect(loadMigrations(migrations).map(({ name }) => name)).toEqual(["001_crm"]);
|
||||||
|
expect(await migrate(db, migrations)).toEqual(["001_crm"]);
|
||||||
|
expect(await migrate(db, migrations)).toEqual([]);
|
||||||
|
await seed(db);
|
||||||
|
await seed(db);
|
||||||
|
|
||||||
|
expect(await db.one<{ count: number }>("SELECT COUNT(*) count FROM crm_contacts")).toEqual({
|
||||||
|
count: 2,
|
||||||
|
});
|
||||||
|
expect(await db.one<{ count: number }>("SELECT COUNT(*) count FROM crm_deals")).toEqual({
|
||||||
|
count: 1,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
await db.all(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='table' AND name IN ('_wrn_authz_assignment','_wrn_authz_grant') ORDER BY name",
|
||||||
|
),
|
||||||
|
).toHaveLength(2);
|
||||||
|
await db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("authentication uses SQL, plugin migrations, signup/login components, and protected routes", () => {
|
||||||
|
expect(source("app/lib/auth.ts")).toContain("SqlAuthStore");
|
||||||
|
expect(source("wrnexus.config.ts")).toContain("migrations: true");
|
||||||
|
expect(source("app/pages/login.wrn")).toContain("<SignIn");
|
||||||
|
expect(source("app/pages/signup.wrn")).toContain("<SignUp");
|
||||||
|
expect(source("app/middleware/protected.ts")).toContain('requireAuth({ loginPath: "/login" })');
|
||||||
|
expect(source("app/pages/dashboard.wrn")).toContain("/api/auth/logout");
|
||||||
|
expect(source("app/pages/login.wrn")).toContain("AuthSplitLayout");
|
||||||
|
expect(source("app/pages/login.wrn")).toContain('data-slot="form"');
|
||||||
|
expect(source("app/pages/signup.wrn")).not.toContain("Already registered?");
|
||||||
|
expect(source("wrnexus.config.ts")).toContain('entry: "app/styles/global.css"');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("authorization is database-backed and guards both APIs and administration", () => {
|
||||||
|
expect(source("app/middleware/authz.ts")).toContain("dbPermissionStore(getDb())");
|
||||||
|
expect(source("app/lib/auth.ts")).toContain('assignRole(user.id, "sales-rep")');
|
||||||
|
expect(source("app/api/contacts.ts")).toContain('can(ctx, "contact:write")');
|
||||||
|
expect(source("app/api/deals.ts")).toContain('can(ctx, "deal:read")');
|
||||||
|
expect(source("app/middleware/protected.ts")).toContain('can(ctx, "admin:access")');
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the app includes public, authentication, and protected CRM pages", () => {
|
||||||
|
for (const page of [
|
||||||
|
"index",
|
||||||
|
"pricing",
|
||||||
|
"login",
|
||||||
|
"signup",
|
||||||
|
"dashboard",
|
||||||
|
"contacts",
|
||||||
|
"deals",
|
||||||
|
"admin",
|
||||||
|
"forbidden",
|
||||||
|
]) {
|
||||||
|
expect(source(`app/pages/${page}.wrn`)).toContain("page ");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("protected CRM data is loaded on the server without browser API bindings", () => {
|
||||||
|
for (const page of ["contacts", "deals", "dashboard"]) {
|
||||||
|
const contents = source(`app/pages/${page}.wrn`);
|
||||||
|
expect(contents).toContain("load server");
|
||||||
|
expect(contents).not.toContain(' api="');
|
||||||
|
}
|
||||||
|
expect(source("app/api/dashboard.ts")).toContain("pipeline_value_cents");
|
||||||
|
expect(source("app/lib/workspace.ts")).toContain("INSERT OR IGNORE INTO crm_contacts");
|
||||||
|
expect(source("app/lib/auth.ts")).toContain("ensureCrmWorkspace(user.id)");
|
||||||
|
expect(source("app/middleware/protected.ts")).toContain('new URL("/forbidden", ctx.url)');
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": { "noEmit": true },
|
||||||
|
"include": ["app/**/*.ts", "test/**/*.ts", "wrnexus.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { join } from "node:path";
|
||||||
|
import type { AuthConfig } from "@wrnexus/auth";
|
||||||
|
import type { AppConfig } from "@wrnexus/styles";
|
||||||
|
import { auth } from "./app/lib/auth.ts";
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
seo: {
|
||||||
|
title: "Northstar CRM",
|
||||||
|
titleTemplate: "%s | Northstar CRM",
|
||||||
|
description: "A complete WrNexus SQLite CRM example.",
|
||||||
|
canonicalBase: "http://localhost:3000",
|
||||||
|
},
|
||||||
|
theme: { palette: "blue", default: "light" },
|
||||||
|
db: { driver: "sqlite", url: "file:./crm.sqlite" },
|
||||||
|
styles: {
|
||||||
|
entry: "app/styles/global.css",
|
||||||
|
async process({ entryPath, appRoot, mode }) {
|
||||||
|
const args = ["@tailwindcss/cli", "-i", entryPath!];
|
||||||
|
if (mode === "production") args.push("--minify");
|
||||||
|
return await Bun.$.cwd(appRoot)`bunx ${args}`.text();
|
||||||
|
},
|
||||||
|
failureMode: "throw",
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
engine: auth,
|
||||||
|
routes: true,
|
||||||
|
middleware: true,
|
||||||
|
components: true,
|
||||||
|
migrations: true,
|
||||||
|
baseUrl: "http://localhost:3000",
|
||||||
|
},
|
||||||
|
profiles: {
|
||||||
|
test: { db: { driver: "sqlite", url: `file:${join(import.meta.dir, ".tmp-test.sqlite")}` } },
|
||||||
|
},
|
||||||
|
security: { cors: { enabled: false } },
|
||||||
|
} satisfies AppConfig & { auth: AuthConfig };
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -25,17 +25,17 @@ component SignIn {
|
|||||||
class = ""
|
class = ""
|
||||||
}
|
}
|
||||||
view {
|
view {
|
||||||
<section {...attrs} data-wrnexus-runtime="auth" data-auth-sign-in data-mfa-href='{mfaHref}' class='w-full max-w-md rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'>
|
<section {...attrs} data-wrnexus-runtime="auth" data-auth-sign-in data-mfa-href='{mfaHref}' class='wrn-auth-sign-in w-full max-w-md rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'>
|
||||||
<header class="mb-6 space-y-1.5">
|
<header class="wrn-auth-sign-in__header mb-6 space-y-1.5">
|
||||||
<h1 class="m-0 text-2xl font-semibold tracking-tight">{title}</h1>
|
<h1 class="m-0 text-2xl font-semibold tracking-tight">{title}</h1>
|
||||||
<p class="m-0 text-sm text-[var(--wrn-color-muted)]">{description}</p>
|
<p class="m-0 text-sm text-[var(--wrn-color-muted)]">{description}</p>
|
||||||
</header>
|
</header>
|
||||||
<form method="post" action='{action}' data-schema='{schema}' data-redirect='{redirect}' novalidate class="space-y-4">
|
<form method="post" action='{action}' data-schema='{schema}' data-redirect='{redirect}' novalidate class="wrn-auth-sign-in__form">
|
||||||
<input type="hidden" name="returnTo" value='{returnTo}' />
|
<input type="hidden" name="returnTo" value='{returnTo}' />
|
||||||
<input type="hidden" name="deviceFingerprint" value="" />
|
<input type="hidden" name="deviceFingerprint" value="" />
|
||||||
<input type="hidden" name="deviceName" value="" />
|
<input type="hidden" name="deviceName" value="" />
|
||||||
<Input id="auth-sign-in-identifier" name="identifier" type="text" label="{identifierLabel}" autocomplete="username webauthn" placeholder="{identifierPlaceholder}" icon="icon-[lucide--at-sign]" color="{color}" size="{size}" />
|
<Input id="auth-sign-in-identifier" name="identifier" type="text" label="{identifierLabel}" autocomplete="username webauthn" placeholder="{identifierPlaceholder}" icon="icon-[lucide--at-sign]" color="{color}" size="{size}" />
|
||||||
<div class="space-y-1.5">
|
<div>
|
||||||
<TogglePassword
|
<TogglePassword
|
||||||
label="{passwordLabel}"
|
label="{passwordLabel}"
|
||||||
cornerHint="Forgot password?"
|
cornerHint="Forgot password?"
|
||||||
@@ -48,9 +48,7 @@ component SignIn {
|
|||||||
size="{size}"
|
size="{size}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if showRemember}
|
{#if showRemember}<div class="wrn-auth-sign-in__remember"><Checkbox id="auth-sign-in-remember" name="rememberDevice" label="Trust this device" color="{color}" size="{size}" /></div>{/if}
|
||||||
<Checkbox id="auth-sign-in-remember" name="rememberDevice" label="Trust this device" color="{color}" size="{size}" />
|
|
||||||
{/if}
|
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p>
|
<p data-error="_form" role="alert" class="m-0 hidden rounded-[var(--wrn-radius-sm)] bg-[color-mix(in_srgb,var(--wrn-color-danger)_10%,transparent)] p-3 text-sm text-[var(--wrn-color-danger)]"></p>
|
||||||
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
<Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" />
|
||||||
@@ -64,4 +62,18 @@ component SignIn {
|
|||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
style {
|
||||||
|
.wrn-auth-sign-in__header { margin-bottom: 1.5rem; }
|
||||||
|
.wrn-auth-sign-in__header h1 { font-size: 1.75rem; line-height: 1.15; }
|
||||||
|
.wrn-auth-sign-in__header p { margin-top: 0.35rem; font-size: 0.9375rem; line-height: 1.5; }
|
||||||
|
.wrn-auth-sign-in__form { display: grid; gap: 1.15rem; }
|
||||||
|
.wrn-auth-sign-in__form > * { margin: 0 !important; }
|
||||||
|
.wrn-auth-sign-in__remember { margin-top: -0.25rem !important; }
|
||||||
|
.wrn-auth-sign-in__form .wrn-next__field-heading label,
|
||||||
|
.wrn-auth-sign-in__form .wrn-next__password-heading { font-size: 0.875rem; font-weight: 650; }
|
||||||
|
.wrn-auth-sign-in__form .wrn-next__field-control > input { min-height: 3rem; font-size: 0.875rem; }
|
||||||
|
.wrn-auth-sign-in__form [data-error]:empty { display: none; }
|
||||||
|
.wrn-auth-sign-in__form .wrn-action[data-full-width="true"] { margin-top: 0.15rem !important; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ component SignUp {
|
|||||||
<section
|
<section
|
||||||
{...attrs}
|
{...attrs}
|
||||||
data-wrnexus-runtime="auth"
|
data-wrnexus-runtime="auth"
|
||||||
class='w-full max-w-lg rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'
|
class='wrn-auth-sign-up w-full max-w-lg rounded-[var(--wrn-radius-lg)] border border-[var(--wrn-color-border)] bg-[var(--wrn-color-surface)] p-6 text-[var(--wrn-color-text)] shadow-[var(--wrn-shadow-2)] {class}'
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="mb-6 space-y-1.5"
|
class="mb-6 space-y-1.5"
|
||||||
@@ -50,7 +50,7 @@ component SignUp {
|
|||||||
data-schema='{schema}'
|
data-schema='{schema}'
|
||||||
data-redirect='{redirect}'
|
data-redirect='{redirect}'
|
||||||
novalidate
|
novalidate
|
||||||
class="grid gap-4 sm:grid-cols-2"
|
class="wrn-auth-sign-up__form"
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
id="auth-sign-up-name"
|
id="auth-sign-up-name"
|
||||||
@@ -61,7 +61,6 @@ component SignUp {
|
|||||||
icon="icon-[lucide--user]"
|
icon="icon-[lucide--user]"
|
||||||
color="{color}"
|
color="{color}"
|
||||||
size="{size}"
|
size="{size}"
|
||||||
class="sm:col-span-2"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
@@ -100,7 +99,6 @@ component SignUp {
|
|||||||
icon="icon-[lucide--circle-user-round]"
|
icon="icon-[lucide--circle-user-round]"
|
||||||
color="{color}"
|
color="{color}"
|
||||||
size="{size}"
|
size="{size}"
|
||||||
class="sm:col-span-2"
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -176,15 +174,7 @@ component SignUp {
|
|||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button
|
<div class="wrn-auth-sign-up__submit"><Button type="submit" label="{submitLabel}" variant="solid" color="{color}" size="{size}" fullWidth="true" /></div>
|
||||||
type="submit"
|
|
||||||
label="{submitLabel}"
|
|
||||||
variant="solid"
|
|
||||||
color="{color}"
|
|
||||||
size="{size}"
|
|
||||||
fullWidth="true"
|
|
||||||
class="wrn-auth-sign-up__wide sm:col-span-2"
|
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
@@ -196,9 +186,19 @@ component SignUp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
style {
|
style {
|
||||||
|
.wrn-auth-sign-up > header { margin-bottom: 1.5rem; }
|
||||||
|
.wrn-auth-sign-up > header h1 { font-size: 1.75rem; line-height: 1.15; letter-spacing: -0.025em; }
|
||||||
|
.wrn-auth-sign-up > header p { margin-top: 0.35rem; font-size: 0.9375rem; line-height: 1.5; }
|
||||||
|
.wrn-auth-sign-up__form { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1.15rem; }
|
||||||
|
.wrn-auth-sign-up__form .wrn-next__field-heading label,
|
||||||
|
.wrn-auth-sign-up__form .wrn-next--strong-password > label { font-size: 0.875rem; font-weight: 650; }
|
||||||
|
.wrn-auth-sign-up__form .wrn-next__field-control > input { min-height: 3rem; font-size: 0.875rem; }
|
||||||
|
.wrn-auth-sign-up__form [data-error]:empty { display: none; }
|
||||||
.wrn-auth-sign-up__wide {
|
.wrn-auth-sign-up__wide {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
.wrn-auth-sign-up__submit { width: 100%; margin-top: 0.25rem; }
|
||||||
|
.wrn-auth-sign-up__submit .wrn-btn { min-height: 3rem; font-size: 0.9375rem; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/auth",
|
"name": "@wrnexus/auth",
|
||||||
"version": "0.8.12",
|
"version": "0.8.13",
|
||||||
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
|
|||||||
@@ -72,6 +72,17 @@ test("account forms use the dedicated password components", () => {
|
|||||||
expect(signUp).not.toContain('id="auth-sign-up-password"');
|
expect(signUp).not.toContain('id="auth-sign-up-password"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("account forms own consistent spacing and full-width submit layout", () => {
|
||||||
|
const signIn = readFileSync(join(directory, "SignIn.wrn"), "utf8");
|
||||||
|
const signUp = readFileSync(join(directory, "SignUp.wrn"), "utf8");
|
||||||
|
|
||||||
|
expect(signIn).toContain("wrn-auth-sign-in__form");
|
||||||
|
expect(signIn).toContain("wrn-auth-sign-in__remember");
|
||||||
|
expect(signUp).toContain("wrn-auth-sign-up__form");
|
||||||
|
expect(signUp).toContain("wrn-auth-sign-up__submit");
|
||||||
|
expect(signUp).toContain("grid-template-columns: minmax(0, 1fr)");
|
||||||
|
});
|
||||||
|
|
||||||
test("authentication journeys use themed UI form controls", () => {
|
test("authentication journeys use themed UI form controls", () => {
|
||||||
const expected = {
|
const expected = {
|
||||||
"SignIn.wrn": ["<Input", "<TogglePassword", "<Checkbox", "<Button"],
|
"SignIn.wrn": ["<Input", "<TogglePassword", "<Checkbox", "<Button"],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/cli",
|
"name": "@wrnexus/cli",
|
||||||
"version": "0.8.46",
|
"version": "0.8.47",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
+72
-5
@@ -14,15 +14,23 @@
|
|||||||
* (`databases.<name>`), with files under `app/db/<name>/`.
|
* (`databases.<name>`), with files under `app/db/<name>/`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
||||||
import { join, resolve } from "node:path";
|
import { basename, join, resolve } from "node:path";
|
||||||
import { pathToFileURL } from "node:url";
|
import { pathToFileURL } from "node:url";
|
||||||
|
import {
|
||||||
|
createPluginRunner,
|
||||||
|
discoverPlugins,
|
||||||
|
type PackageMigrationDefinition,
|
||||||
|
} from "@wrnexus/plugin";
|
||||||
import { loadAppConfig, type AppConfig } from "@wrnexus/styles";
|
import { loadAppConfig, type AppConfig } from "@wrnexus/styles";
|
||||||
import {
|
import {
|
||||||
generateQueriesFile,
|
generateQueriesFile,
|
||||||
analyzeMigrations,
|
analyzeMigrations,
|
||||||
|
appliedMigrations,
|
||||||
|
applyMigrations,
|
||||||
loadMigrations,
|
loadMigrations,
|
||||||
migrate,
|
migrate,
|
||||||
|
parseMigration,
|
||||||
parseQueries,
|
parseQueries,
|
||||||
rollback,
|
rollback,
|
||||||
scaffoldMigration,
|
scaffoldMigration,
|
||||||
@@ -42,6 +50,55 @@ function dbBaseOf(appDir: string, dbName: string | null): string {
|
|||||||
return dbName ? join(appDir, "db", dbName) : join(appDir, "db");
|
return dbName ? join(appDir, "db", dbName) : join(appDir, "db");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function packageMigrationName(definition: PackageMigrationDefinition, name?: string): string {
|
||||||
|
const clean = (value: string) => value.trim().replace(/[^a-zA-Z0-9_.-]+/g, "_");
|
||||||
|
return name ? `${clean(definition.id)}__${clean(name)}` : clean(definition.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePackageMigrations(
|
||||||
|
definitions: readonly PackageMigrationDefinition[],
|
||||||
|
database?: string,
|
||||||
|
) {
|
||||||
|
return definitions.flatMap((definition) => {
|
||||||
|
if ((definition.database?.trim() || "default") !== (database?.trim() || "default")) return [];
|
||||||
|
if (definition.source !== undefined) {
|
||||||
|
return [parseMigration(packageMigrationName(definition), definition.source)];
|
||||||
|
}
|
||||||
|
if (!definition.entry || !existsSync(definition.entry)) {
|
||||||
|
throw new Error(`WRN-PLUGIN-MIGRATION-MISSING: ${definition.id} has no readable entry.`);
|
||||||
|
}
|
||||||
|
if (statSync(definition.entry).isDirectory()) {
|
||||||
|
return loadMigrations(definition.entry).map((migration) => ({
|
||||||
|
...migration,
|
||||||
|
name: packageMigrationName(definition, migration.name),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
parseMigration(
|
||||||
|
packageMigrationName(definition, basename(definition.entry, ".sql")),
|
||||||
|
readFileSync(definition.entry, "utf8"),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function packageMigrations(root: string, config: AppConfig, database?: string) {
|
||||||
|
const discovered = await discoverPlugins(root, config.plugins, {
|
||||||
|
includeDevDependencies: true,
|
||||||
|
strict: true,
|
||||||
|
});
|
||||||
|
const runner = createPluginRunner(discovered, {
|
||||||
|
root,
|
||||||
|
mode: "development",
|
||||||
|
command: "dev",
|
||||||
|
metadata: new Map(),
|
||||||
|
warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
|
||||||
|
});
|
||||||
|
await runner.configure(config as Record<string, unknown>);
|
||||||
|
await runner.configResolved(config as Readonly<Record<string, unknown>>);
|
||||||
|
return resolvePackageMigrations((await runner.contributions()).migrations, database);
|
||||||
|
}
|
||||||
|
|
||||||
/** List user tables for the connected database (dialect-aware introspection). */
|
/** List user tables for the connected database (dialect-aware introspection). */
|
||||||
async function listTables(db: import("@wrnexus/db").Db): Promise<string[]> {
|
async function listTables(db: import("@wrnexus/db").Db): Promise<string[]> {
|
||||||
const dialect = db.driver.dialect;
|
const dialect = db.driver.dialect;
|
||||||
@@ -126,6 +183,7 @@ export async function runDbCommand(
|
|||||||
const dbConfig = dbName ? config.databases?.[dbName] : config.db;
|
const dbConfig = dbName ? config.databases?.[dbName] : config.db;
|
||||||
const dbBase = dbBaseOf(appDir, dbName);
|
const dbBase = dbBaseOf(appDir, dbName);
|
||||||
const migrationsDir = join(dbBase, "migrations");
|
const migrationsDir = join(dbBase, "migrations");
|
||||||
|
const contributedMigrations = await packageMigrations(root, config, dbName ?? undefined);
|
||||||
const label = dbName ? ` (db: ${dbName})` : "";
|
const label = dbName ? ` (db: ${dbName})` : "";
|
||||||
const safetyIssues = analyzeMigrations(loadMigrations(migrationsDir));
|
const safetyIssues = analyzeMigrations(loadMigrations(migrationsDir));
|
||||||
|
|
||||||
@@ -242,7 +300,10 @@ export async function runDbCommand(
|
|||||||
`WRN-DB-UNSAFE-MIGRATION: ${blockers.length} breaking rollout operation(s) found. Run 'wrnexus db check' and use an expand/contract migration; --allow-breaking explicitly overrides this gate.`,
|
`WRN-DB-UNSAFE-MIGRATION: ${blockers.length} breaking rollout operation(s) found. Run 'wrnexus db check' and use an expand/contract migration; --allow-breaking explicitly overrides this gate.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const applied = await migrate(db, migrationsDir);
|
const applied = [
|
||||||
|
...(await migrate(db, migrationsDir)),
|
||||||
|
...(await applyMigrations(db, contributedMigrations)),
|
||||||
|
];
|
||||||
console.log(
|
console.log(
|
||||||
applied.length
|
applied.length
|
||||||
? `✓ Applied ${applied.length}${label}:\n ${applied.join("\n ")}`
|
? `✓ Applied ${applied.length}${label}:\n ${applied.join("\n ")}`
|
||||||
@@ -257,8 +318,14 @@ export async function runDbCommand(
|
|||||||
}
|
}
|
||||||
case "status": {
|
case "status": {
|
||||||
const rows = await status(db, migrationsDir);
|
const rows = await status(db, migrationsDir);
|
||||||
if (rows.length === 0) console.log(`No migrations found in ${migrationsDir}.`);
|
const applied = new Set(await appliedMigrations(db));
|
||||||
else for (const r of rows) console.log(` [${r.applied ? "x" : " "}] ${r.name}`);
|
const packageRows = contributedMigrations.map(({ name }) => ({
|
||||||
|
name,
|
||||||
|
applied: applied.has(name),
|
||||||
|
}));
|
||||||
|
const allRows = [...rows, ...packageRows];
|
||||||
|
if (allRows.length === 0) console.log(`No migrations found in ${migrationsDir}.`);
|
||||||
|
else for (const r of allRows) console.log(` [${r.applied ? "x" : " "}] ${r.name}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
mkdirSync,
|
mkdirSync,
|
||||||
readFileSync,
|
readFileSync,
|
||||||
readdirSync,
|
readdirSync,
|
||||||
rmSync,
|
|
||||||
statSync,
|
statSync,
|
||||||
writeFileSync,
|
writeFileSync,
|
||||||
} from "node:fs";
|
} from "node:fs";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/dev-server",
|
"name": "@wrnexus/dev-server",
|
||||||
"version": "0.8.41",
|
"version": "0.8.42",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -1482,13 +1482,27 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
|||||||
});
|
});
|
||||||
const apiCtx = createContext(apiReq, apiUrl);
|
const apiCtx = createContext(apiReq, apiUrl);
|
||||||
apiCtx.locals = ctx.locals;
|
apiCtx.locals = ctx.locals;
|
||||||
|
apiCtx.user = ctx.user;
|
||||||
|
apiCtx.tenant = ctx.tenant;
|
||||||
|
apiCtx.tracer = ctx.tracer;
|
||||||
|
apiCtx.ip = ctx.ip;
|
||||||
|
apiCtx.lang = ctx.lang;
|
||||||
|
apiCtx.t = ctx.t;
|
||||||
|
apiCtx.params = ctx.params;
|
||||||
apiCtx.cookies = ctx.cookies;
|
apiCtx.cookies = ctx.cookies;
|
||||||
apiCtx.session = ctx.session;
|
apiCtx.session = ctx.session;
|
||||||
apiCtx.localStorage = ctx.localStorage;
|
apiCtx.localStorage = ctx.localStorage;
|
||||||
|
|
||||||
const apiRes = await handleApi(apiCtx);
|
const apiRes = await handleApi(apiCtx);
|
||||||
if (!apiRes.ok) {
|
if (!apiRes.ok) {
|
||||||
throw new Error(`API route ${apiUrl.pathname} returned ${apiRes.status}`);
|
const contentType = apiRes.headers.get("content-type") ?? "";
|
||||||
|
const data = contentType.includes("application/json")
|
||||||
|
? await apiRes.json().catch(() => undefined)
|
||||||
|
: await apiRes.text().catch(() => undefined);
|
||||||
|
throw Object.assign(new Error(`API route ${apiUrl.pathname} returned ${apiRes.status}`), {
|
||||||
|
status: apiRes.status,
|
||||||
|
data,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const contentType = apiRes.headers.get("content-type") ?? "";
|
const contentType = apiRes.headers.get("content-type") ?? "";
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { afterEach, expect, test } from "bun:test";
|
||||||
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { buildRouter } from "@wrnexus/router";
|
||||||
|
import { createHandlers, type RuntimeDeps } from "../src/runtime.ts";
|
||||||
|
|
||||||
|
const roots: string[] = [];
|
||||||
|
afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })));
|
||||||
|
|
||||||
|
test("in-process api failures preserve status and response data for server error sections", async () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "wrnexus-api-error-context-"));
|
||||||
|
roots.push(root);
|
||||||
|
const app = join(root, "app");
|
||||||
|
mkdirSync(join(app, "pages"), { recursive: true });
|
||||||
|
mkdirSync(join(app, "api"), { recursive: true });
|
||||||
|
writeFileSync(join(app, "pages", "probe.ts"), "export default () => '';\n");
|
||||||
|
writeFileSync(join(app, "api", "failure.ts"), "export const GET = () => null;\n");
|
||||||
|
|
||||||
|
const handlers = createHandlers({
|
||||||
|
mode: "production",
|
||||||
|
hmr: false,
|
||||||
|
router: buildRouter(app),
|
||||||
|
loadModule: async (file) =>
|
||||||
|
file.endsWith(`${join("api", "failure")}.ts`)
|
||||||
|
? {
|
||||||
|
GET: () => Response.json({ error: "denied", reason: "policy" }, { status: 403 }),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
__wrnexusLoad: async (ctx: {
|
||||||
|
__wrnexusCallApi: (path: string, method: string) => Promise<unknown>;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
await ctx.__wrnexusCallApi("/api/failure", "GET");
|
||||||
|
return { caught: null };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
caught: {
|
||||||
|
status: (error as { status?: unknown }).status,
|
||||||
|
data: (error as { data?: unknown }).data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
default: (ctx: { caught: unknown }) => `<pre>${JSON.stringify(ctx.caught)}</pre>`,
|
||||||
|
},
|
||||||
|
getMiddleware: async () => [],
|
||||||
|
assets: { serve: async () => null },
|
||||||
|
} satisfies RuntimeDeps);
|
||||||
|
|
||||||
|
const response = await handlers.fetch(new Request("https://example.test/probe"), {
|
||||||
|
upgrade: () => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response?.status).toBe(200);
|
||||||
|
expect(await response?.text()).toContain(
|
||||||
|
JSON.stringify({ status: 403, data: { error: "denied", reason: "policy" } }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("in-process api calls inherit authenticated request context", async () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "wrnexus-api-auth-context-"));
|
||||||
|
roots.push(root);
|
||||||
|
const app = join(root, "app");
|
||||||
|
mkdirSync(join(app, "pages"), { recursive: true });
|
||||||
|
mkdirSync(join(app, "api"), { recursive: true });
|
||||||
|
writeFileSync(join(app, "pages", "probe.ts"), "export default () => '';\n");
|
||||||
|
writeFileSync(join(app, "api", "identity.ts"), "export const GET = () => null;\n");
|
||||||
|
|
||||||
|
const handlers = createHandlers({
|
||||||
|
mode: "production",
|
||||||
|
hmr: false,
|
||||||
|
router: buildRouter(app),
|
||||||
|
loadModule: async (file) =>
|
||||||
|
file.endsWith(`${join("api", "identity")}.ts`)
|
||||||
|
? { GET: (ctx: { user?: unknown }) => Response.json({ user: ctx.user }) }
|
||||||
|
: {
|
||||||
|
__wrnexusLoad: async (ctx: {
|
||||||
|
__wrnexusCallApi: (path: string, method: string) => Promise<unknown>;
|
||||||
|
}) => ({ identity: await ctx.__wrnexusCallApi("/api/identity", "GET") }),
|
||||||
|
default: (ctx: { identity: unknown }) => `<pre>${JSON.stringify(ctx.identity)}</pre>`,
|
||||||
|
},
|
||||||
|
getMiddleware: async () => [
|
||||||
|
(ctx, next) => {
|
||||||
|
ctx.user = { id: "authenticated-user" };
|
||||||
|
return next();
|
||||||
|
},
|
||||||
|
],
|
||||||
|
assets: { serve: async () => null },
|
||||||
|
} satisfies RuntimeDeps);
|
||||||
|
|
||||||
|
const response = await handlers.fetch(new Request("https://example.test/probe"), {
|
||||||
|
upgrade: () => false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response?.status).toBe(200);
|
||||||
|
expect(await response?.text()).toContain(JSON.stringify({ user: { id: "authenticated-user" } }));
|
||||||
|
});
|
||||||
@@ -32,11 +32,14 @@ component StrongPassword {
|
|||||||
required: boolean = false
|
required: boolean = false
|
||||||
invalid: boolean = false
|
invalid: boolean = false
|
||||||
validationMessage: string = ""
|
validationMessage: string = ""
|
||||||
|
showLabel: string = "Show password"
|
||||||
|
hideLabel: string = "Hide password"
|
||||||
class: string = ""
|
class: string = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
state password = value
|
state password = value
|
||||||
state detailsOpen: boolean = false
|
state detailsOpen: boolean = false
|
||||||
|
state revealed: boolean = false
|
||||||
|
|
||||||
functions {
|
functions {
|
||||||
shared function hasMinimumLength() {
|
shared function hasMinimumLength() {
|
||||||
@@ -113,6 +116,12 @@ component StrongPassword {
|
|||||||
return emptyLabel
|
return emptyLabel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shared function toggleVisibility() {
|
||||||
|
if (!disabled && !readonly) {
|
||||||
|
revealed = !revealed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
view {
|
view {
|
||||||
@@ -124,10 +133,11 @@ component StrongPassword {
|
|||||||
>
|
>
|
||||||
<label for="{name}-strong-password">{label}</label>
|
<label for="{name}-strong-password">{label}</label>
|
||||||
<div class="wrn-next__strong-password-anchor">
|
<div class="wrn-next__strong-password-anchor">
|
||||||
|
<span class="wrn-next__strong-password-leading-icon" aria-hidden="true"></span>
|
||||||
<input
|
<input
|
||||||
{...attrs}
|
{...attrs}
|
||||||
id="{name}-strong-password"
|
id="{name}-strong-password"
|
||||||
type="password"
|
type="{revealed ? 'text' : 'password'}"
|
||||||
name="{name}"
|
name="{name}"
|
||||||
value="{password}"
|
value="{password}"
|
||||||
placeholder="{placeholder}"
|
placeholder="{placeholder}"
|
||||||
@@ -143,6 +153,15 @@ component StrongPassword {
|
|||||||
@input="password = event.target.value; output.input({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() }); output.strength({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })"
|
@input="password = event.target.value; output.input({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() }); output.strength({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })"
|
||||||
@change="output.change({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })"
|
@change="output.change({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="wrn-next__strong-password-toggle"
|
||||||
|
aria-label="{revealed ? hideLabel : showLabel}"
|
||||||
|
title="{revealed ? hideLabel : showLabel}"
|
||||||
|
aria-pressed="{revealed ? 'true' : 'false'}"
|
||||||
|
disabled="{disabled || readonly}"
|
||||||
|
@click="toggleVisibility()"
|
||||||
|
><span aria-hidden="true"></span></button>
|
||||||
|
|
||||||
{#if presentation === "popover"}
|
{#if presentation === "popover"}
|
||||||
<div
|
<div
|
||||||
@@ -236,19 +255,72 @@ component StrongPassword {
|
|||||||
|
|
||||||
.wrn-next--strong-password input {
|
.wrn-next--strong-password input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 2.65rem;
|
min-height: 3rem;
|
||||||
padding: 0.7rem 0.85rem;
|
padding: 0.75rem 3rem 0.75rem 2.75rem;
|
||||||
border: 1px solid var(--wrn-color-border);
|
border: 1px solid var(--wrn-color-border);
|
||||||
border-radius: var(--wrn-radius-sm);
|
border-radius: var(--wrn-radius-sm);
|
||||||
outline: 0;
|
outline: 0;
|
||||||
color: var(--wrn-color-text);
|
color: var(--wrn-color-text);
|
||||||
background: var(--wrn-color-surface);
|
background: var(--wrn-color-surface);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
font-size: 0.875rem;
|
||||||
transition:
|
transition:
|
||||||
border-color 160ms ease,
|
border-color 160ms ease,
|
||||||
box-shadow 160ms ease;
|
box-shadow 160ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrn-next__strong-password-leading-icon,
|
||||||
|
.wrn-next__strong-password-toggle {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
z-index: 2;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
.wrn-next__strong-password-leading-icon {
|
||||||
|
left: 0.9rem;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
color: var(--wrn-color-muted);
|
||||||
|
background: currentColor;
|
||||||
|
pointer-events: none;
|
||||||
|
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect width='18' height='11' x='3' y='11' rx='2' ry='2'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||||
|
}
|
||||||
|
.wrn-next__strong-password-toggle {
|
||||||
|
right: 0.45rem;
|
||||||
|
display: grid;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0;
|
||||||
|
place-items: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: calc(var(--wrn-radius-sm) * 0.72);
|
||||||
|
color: var(--wrn-color-muted);
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.wrn-next__strong-password-toggle:hover,
|
||||||
|
.wrn-next__strong-password-toggle:focus-visible {
|
||||||
|
color: var(--wrn-component-color);
|
||||||
|
background: color-mix(in srgb, var(--wrn-component-color) 10%, transparent);
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
.wrn-next__strong-password-toggle > span {
|
||||||
|
width: 1.05rem;
|
||||||
|
height: 1.05rem;
|
||||||
|
background: currentColor;
|
||||||
|
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.06 12.35a1 1 0 0 1 0-.7C3.7 7.6 7.64 5 12 5c4.36 0 8.3 2.6 9.94 6.65a1 1 0 0 1 0 .7C20.3 16.4 16.36 19 12 19c-4.36 0-8.3-2.6-9.94-6.65Z'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||||
|
}
|
||||||
|
.wrn-next__strong-password-toggle[aria-pressed="true"]::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 0.12rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: currentColor;
|
||||||
|
content: "";
|
||||||
|
transform: rotate(45deg);
|
||||||
|
box-shadow: 0 0 0 1px var(--wrn-color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
.wrn-next--strong-password input:focus-visible {
|
.wrn-next--strong-password input:focus-visible {
|
||||||
border-color: var(--wrn-component-color);
|
border-color: var(--wrn-component-color);
|
||||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-component-color) 16%, transparent);
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-component-color) 16%, transparent);
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ component TogglePassword {
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="wrn-next__password-control">
|
<div class="wrn-next__password-control">
|
||||||
|
<span class="wrn-next__password-leading-icon" aria-hidden="true"></span>
|
||||||
<input
|
<input
|
||||||
{...attrs}
|
{...attrs}
|
||||||
id="{name}-password"
|
id="{name}-password"
|
||||||
@@ -229,19 +230,32 @@ component TogglePassword {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 2.75em;
|
min-height: 2.75em;
|
||||||
padding: 0.65em 3em 0.65em 0.8em;
|
min-height: 3rem;
|
||||||
|
padding: 0.75rem 3rem 0.75rem 2.75rem;
|
||||||
border: 1px solid var(--wrn-color-border);
|
border: 1px solid var(--wrn-color-border);
|
||||||
border-radius: var(--wrn-radius-sm);
|
border-radius: var(--wrn-radius-sm);
|
||||||
outline: 0;
|
outline: 0;
|
||||||
color: var(--wrn-color-text);
|
color: var(--wrn-color-text);
|
||||||
background: var(--wrn-color-bg);
|
background: var(--wrn-color-surface);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 0.8125rem;
|
font-size: 0.875rem;
|
||||||
transition:
|
transition:
|
||||||
border-color var(--wrn-motion-base) var(--wrn-ease-standard),
|
border-color var(--wrn-motion-base) var(--wrn-ease-standard),
|
||||||
box-shadow var(--wrn-motion-base) var(--wrn-ease-standard);
|
box-shadow var(--wrn-motion-base) var(--wrn-ease-standard);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrn-next__password-leading-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 0.9rem;
|
||||||
|
z-index: 1;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
color: var(--wrn-color-muted);
|
||||||
|
background: currentColor;
|
||||||
|
pointer-events: none;
|
||||||
|
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect width='18' height='11' x='3' y='11' rx='2' ry='2'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E") center / contain no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
.wrn-next__password-control > input:focus-visible {
|
.wrn-next__password-control > input:focus-visible {
|
||||||
border-color: var(--wrn-component-color);
|
border-color: var(--wrn-component-color);
|
||||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-component-color) 18%, transparent);
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--wrn-component-color) 18%, transparent);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ label: string = "Button"
|
|||||||
view {
|
view {
|
||||||
<span
|
<span
|
||||||
class="wrn-action {fullWidth ? 'w-full' : ''} {class}"
|
class="wrn-action {fullWidth ? 'w-full' : ''} {class}"
|
||||||
|
data-full-width="{fullWidth ? 'true' : 'false'}"
|
||||||
>
|
>
|
||||||
{#if as === "a" || (as === "" && href)}
|
{#if as === "a" || (as === "" && href)}
|
||||||
<a
|
<a
|
||||||
@@ -414,6 +415,11 @@ label: string = "Button"
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrn-action[data-full-width="true"],
|
||||||
|
.wrn-action[data-full-width="true"] > .wrn-btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.wrn-btn--pill {
|
.wrn-btn--pill {
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.8.20",
|
"version": "0.8.21",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -263,6 +263,18 @@ test("auth form uses package schemas, disables native validation, and renders a
|
|||||||
expect(html).toContain('fullWidth="true"');
|
expect(html).toContain('fullWidth="true"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("password controls provide matching lock and visibility affordances", () => {
|
||||||
|
const toggle = readFileSync(uiComponentPath("TogglePassword"), "utf8");
|
||||||
|
const strong = readFileSync(uiComponentPath("StrongPassword"), "utf8");
|
||||||
|
const button = readFileSync(uiComponentPath("Button"), "utf8");
|
||||||
|
|
||||||
|
expect(toggle).toContain("wrn-next__password-leading-icon");
|
||||||
|
expect(strong).toContain("wrn-next__strong-password-leading-icon");
|
||||||
|
expect(strong).toContain("wrn-next__strong-password-toggle");
|
||||||
|
expect(strong).toContain("type=\"{revealed ? 'text' : 'password'}\"");
|
||||||
|
expect(button).toContain("data-full-width=\"{fullWidth ? 'true' : 'false'}\"");
|
||||||
|
});
|
||||||
|
|
||||||
test("footer supports typed entries, responsive columns, pre/post slots, and events", () => {
|
test("footer supports typed entries, responsive columns, pre/post slots, and events", () => {
|
||||||
const source = readFileSync(uiComponentPath("Footer"), "utf8");
|
const source = readFileSync(uiComponentPath("Footer"), "utf8");
|
||||||
expect(source).toMatch(/<slot\s+name="pre-footer"\s*>/);
|
expect(source).toMatch(/<slot\s+name="pre-footer"\s*>/);
|
||||||
|
|||||||
Reference in New Issue
Block a user