@reqquest/ui 1.17.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/app/env.template +5 -0
  2. package/app/runtime/apply-env.sh +10 -0
  3. package/app/runtime/nginx.conf +37 -0
  4. package/app/src/app.css +21 -0
  5. package/app/src/app.d.ts +13 -0
  6. package/app/src/app.html +12 -0
  7. package/app/src/internal/api.ts +1707 -0
  8. package/app/src/internal/appreq-utils.ts +33 -0
  9. package/app/src/internal/components/AnnouncementBanner.svelte +47 -0
  10. package/app/src/internal/components/AppRequestCard.svelte +173 -0
  11. package/app/src/internal/components/ApplicantOptOutModal.svelte +78 -0
  12. package/app/src/internal/components/ApplicantProgramList.svelte +276 -0
  13. package/app/src/internal/components/ApplicantProgramListTooltip.svelte +44 -0
  14. package/app/src/internal/components/ApplicantPromptPage.svelte +136 -0
  15. package/app/src/internal/components/ApplicantPromptSkeleton.svelte +12 -0
  16. package/app/src/internal/components/ApplicationDetailsView.svelte +369 -0
  17. package/app/src/internal/components/ButtonLoadingIcon.svelte +28 -0
  18. package/{dist → app/src/internal}/components/DelayedSkeleton.svelte +2 -2
  19. package/app/src/internal/components/FieldNestedMultiselect.svelte +270 -0
  20. package/app/src/internal/components/IntroPanel.svelte +41 -0
  21. package/app/src/internal/components/MissingDefinitionNotification.svelte +20 -0
  22. package/app/src/internal/components/PeriodPanel.svelte +125 -0
  23. package/app/src/internal/components/ProgramStatusCell.svelte +134 -0
  24. package/app/src/internal/components/RenderDisplayComponent.svelte +42 -0
  25. package/app/src/internal/components/ReviewerActions.svelte +171 -0
  26. package/app/src/internal/components/ReviewerQuestions.svelte +508 -0
  27. package/app/src/internal/components/StatusMessageList.svelte +54 -0
  28. package/app/src/internal/components/SystemDeterminationNotice.svelte +37 -0
  29. package/app/src/internal/components/TabLinks.svelte +265 -0
  30. package/app/src/internal/components/WarningIconYellow.svelte +22 -0
  31. package/app/src/internal/components/index.ts +19 -0
  32. package/app/src/internal/csv.ts +30 -0
  33. package/app/src/internal/index.ts +8 -0
  34. package/app/src/internal/prompt-save-queue.ts +91 -0
  35. package/app/src/internal/prompt-utils.ts +40 -0
  36. package/app/src/internal/status-utils.ts +503 -0
  37. package/app/src/internal/stores/IStateStore.ts +5 -0
  38. package/app/src/internal/util.ts +27 -0
  39. package/app/src/routes/+error.svelte +4 -0
  40. package/app/src/routes/+layout.svelte +134 -0
  41. package/app/src/routes/+layout.ts +18 -0
  42. package/app/src/routes/+page.ts +14 -0
  43. package/app/src/routes/announcement/+page.svelte +164 -0
  44. package/app/src/routes/announcement/+page.ts +16 -0
  45. package/app/src/routes/dashboards/applicant/+page.svelte +494 -0
  46. package/app/src/routes/dashboards/applicant/+page.ts +95 -0
  47. package/app/src/routes/dashboards/reviewer/+page.svelte +143 -0
  48. package/app/src/routes/dashboards/reviewer/+page.ts +43 -0
  49. package/app/src/routes/periods/+page.svelte +158 -0
  50. package/app/src/routes/periods/+page.ts +13 -0
  51. package/app/src/routes/periods/[id]/configure/+page.svelte +150 -0
  52. package/app/src/routes/periods/[id]/configure/+page.ts +10 -0
  53. package/app/src/routes/requests/+page.svelte +246 -0
  54. package/app/src/routes/requests/+page.ts +24 -0
  55. package/app/src/routes/requests/[id]/+layout.ts +11 -0
  56. package/app/src/routes/requests/[id]/accept/+layout.svelte +80 -0
  57. package/app/src/routes/requests/[id]/accept/+layout.ts +12 -0
  58. package/app/src/routes/requests/[id]/accept/+page.ts +13 -0
  59. package/app/src/routes/requests/[id]/accept/[promptId]/+page.svelte +7 -0
  60. package/app/src/routes/requests/[id]/accept/[promptId]/+page.ts +10 -0
  61. package/app/src/routes/requests/[id]/accept/review/+page.svelte +87 -0
  62. package/app/src/routes/requests/[id]/apply/+layout.svelte +181 -0
  63. package/app/src/routes/requests/[id]/apply/+layout.ts +12 -0
  64. package/app/src/routes/requests/[id]/apply/+page.ts +14 -0
  65. package/app/src/routes/requests/[id]/apply/ProgressNavContainer.svelte +42 -0
  66. package/app/src/routes/requests/[id]/apply/[promptId]/+page.svelte +23 -0
  67. package/app/src/routes/requests/[id]/apply/[promptId]/+page.ts +11 -0
  68. package/app/src/routes/requests/[id]/apply/programs/+page.svelte +71 -0
  69. package/app/src/routes/requests/[id]/apply/review/+page.svelte +102 -0
  70. package/app/src/routes/requests/[id]/approve/+layout.svelte +114 -0
  71. package/app/src/routes/requests/[id]/approve/+page.ts +14 -0
  72. package/app/src/routes/requests/[id]/approve/ApproveLayout.svelte +164 -0
  73. package/app/src/routes/requests/[id]/approve/[programKey]/+page.svelte +396 -0
  74. package/app/src/routes/requests/[id]/approve/[programKey]/+page.ts +15 -0
  75. package/app/src/routes/requests/[id]/approve/activity/+page.svelte +66 -0
  76. package/app/src/routes/requests/[id]/approve/activity/+page.ts +12 -0
  77. package/app/src/routes/requests/[id]/approve/export/+page.ts +12 -0
  78. package/app/src/routes/requests/[id]/approve/export/+page@[id].svelte +41 -0
  79. package/app/src/routes/requests/[id]/export/+page.svelte +32 -0
  80. package/app/src/routes/requests/[id]/export/+page.ts +12 -0
  81. package/app/src/routes/roles/+layout.svelte +16 -0
  82. package/app/src/routes/roles/+layout.ts +14 -0
  83. package/app/src/routes/roles/+page.svelte +225 -0
  84. package/app/src/routes/roles/+page.ts +13 -0
  85. package/app/src/routes/roles/[id]/+layout.ts +10 -0
  86. package/app/src/routes/roles/[id]/+page.svelte +335 -0
  87. package/app/src/routes/roles/[id]/+page.ts +10 -0
  88. package/app/src/routes/roles/[id]/ControlWithTooltip.svelte +15 -0
  89. package/app/src/routes/roles/[id]/TagsWithTooltip.svelte +42 -0
  90. package/app/src/routes/roles/users/+layout.ts +13 -0
  91. package/app/src/routes/roles/users/+page.svelte +46 -0
  92. package/app/src/routes/roles/users/+page.ts +90 -0
  93. package/app/static/favicon.png +0 -0
  94. package/bin/reqquest-ui.js +117 -0
  95. package/cli/config.js +105 -0
  96. package/cli/sync.js +296 -0
  97. package/cli/util.js +29 -0
  98. package/dist/components.md +5 -4
  99. package/dist/registry.d.ts +17 -0
  100. package/dist/registry.js +3 -0
  101. package/package.json +29 -15
  102. package/dist/components/DelayedSkeleton.svelte.d.ts +0 -21
package/cli/config.js ADDED
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The SvelteKit and Vite configuration for a downstream ReqQuest UI.
3
+ *
4
+ * `reqquest-ui sync` writes .reqquest/svelte.config.js and .reqquest/vite.config.js as two-line stubs
5
+ * that import these functions by absolute path. Keeping the real configuration here, inside the
6
+ * package, means every plugin below resolves from @reqquest/ui's own dependencies - the downstream
7
+ * project never declares vite, the adapter, tailwind or the preprocessors, so their versions are
8
+ * whatever this release of @reqquest/ui was built and tested with.
9
+ *
10
+ * Mirrors ui/svelte.config.js and ui/vite.config.ts; change them together.
11
+ */
12
+ import { join } from 'node:path'
13
+ import adapter from '@sveltejs/adapter-static'
14
+ import { sveltekit } from '@sveltejs/kit/vite'
15
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
16
+ import carbonSvelteTailwind from '@txstate-mws/carbon-svelte/tailwind'
17
+ import autoprefixer from 'autoprefixer'
18
+ import { optimizeImports } from 'carbon-preprocess-svelte'
19
+ import tailwindcss from 'tailwindcss'
20
+ import { packageDir } from './util.js'
21
+
22
+ /**
23
+ * @param {{ projectRoot: string, appRoot: string, basePath?: string }} opts
24
+ * projectRoot - the downstream project (holds src/local, src/routes, package.json)
25
+ * appRoot - the generated .reqquest/ directory SvelteKit runs in
26
+ * @returns {import('@sveltejs/kit').Config}
27
+ */
28
+ export function svelteConfig ({ projectRoot, appRoot, basePath = '' }) {
29
+ return {
30
+ preprocess: [vitePreprocess(), optimizeImports()],
31
+ kit: {
32
+ adapter: adapter({
33
+ pages: join(projectRoot, 'build'),
34
+ assets: join(projectRoot, 'build'),
35
+ fallback: 'index.html',
36
+ precompress: true
37
+ }),
38
+ output: {
39
+ preloadStrategy: 'preload-mjs'
40
+ },
41
+ paths: {
42
+ base: basePath
43
+ },
44
+ files: {
45
+ // $lib keeps meaning the project's own src/lib, as it would in any SvelteKit app
46
+ lib: join(projectRoot, 'src/lib')
47
+ },
48
+ alias: {
49
+ $internal: 'src/internal',
50
+ // the downstream registry is not copied: it is aliased in place so the editor, HMR and
51
+ // svelte-check all work on the files the developer actually edits
52
+ $local: join(projectRoot, 'src/local')
53
+ },
54
+ typescript: {
55
+ // SvelteKit's generated tsconfig only includes the app root, which src/local and src/lib sit outside of
56
+ config (tsconfig) {
57
+ for (const dir of ['src/local', 'src/lib']) {
58
+ const abs = join(projectRoot, dir)
59
+ tsconfig.include.push(`${abs}/**/*.ts`, `${abs}/**/*.js`, `${abs}/**/*.svelte`)
60
+ }
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ /**
68
+ * @param {{ projectRoot: string, appRoot: string }} opts
69
+ * @returns {import('vite').UserConfig}
70
+ */
71
+ export function viteConfig ({ projectRoot, appRoot }) {
72
+ const globs = '**/*.{html,js,svelte,ts}'
73
+ return {
74
+ plugins: [sveltekit()],
75
+ build: {
76
+ sourcemap: true
77
+ },
78
+ optimizeDeps: {
79
+ exclude: ['carbon-icons-svelte']
80
+ },
81
+ server: {
82
+ fs: {
83
+ allow: [projectRoot, packageDir('@txstate-mws/carbon-svelte', appRoot)]
84
+ }
85
+ },
86
+ // inline so that no postcss.config.js / tailwind.config.js is needed in either the project or
87
+ // .reqquest/, and so the plugins resolve from this package rather than the project
88
+ css: {
89
+ postcss: {
90
+ plugins: [
91
+ tailwindcss({
92
+ content: [
93
+ join(appRoot, 'src', globs),
94
+ join(projectRoot, 'src/local', globs),
95
+ join(projectRoot, 'src/lib', globs),
96
+ join(packageDir('@txstate-mws/carbon-svelte', appRoot), globs)
97
+ ],
98
+ plugins: [carbonSvelteTailwind]
99
+ }),
100
+ autoprefixer()
101
+ ]
102
+ }
103
+ }
104
+ }
105
+ }
package/cli/sync.js ADDED
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Assembles .reqquest/ - a complete SvelteKit project - from the app shell shipped in this package
3
+ * plus the downstream project's own files.
4
+ *
5
+ * node_modules/@reqquest/ui/app/src/routes base routes (read-only)
6
+ * + <project>/src/routes developer overrides and additions
7
+ * = .reqquest/src/routes what SvelteKit builds
8
+ *
9
+ * Overlay rules, applied in this order:
10
+ * 1. package.json "reqquest": { "disableRoutes": ["roles/users"] } removes those base route folders
11
+ * 2. a `.reqquest-replace` file in a developer folder removes the base folder of the same path, so
12
+ * the developer's version stands alone (use when an override page needs different load data)
13
+ * 3. every developer file is copied over the base; same path = override, new path = addition
14
+ *
15
+ * src/local is never copied - svelteConfig() aliases $local to it in place. src/internal is base-only:
16
+ * every route depends on it, so letting it be overridden would freeze it as public API.
17
+ *
18
+ * Overriding a base file forks it. reqquest-overrides.lock.json (committed, in the project root)
19
+ * records the hash of each base file at the time it was overridden, so an upgrade that changes the
20
+ * file underneath an override is reported instead of silently diverging.
21
+ *
22
+ * The generated tree is reconciled rather than wiped: unchanged files are not rewritten, so a
23
+ * running dev server only sees what actually changed, and .reqquest/.svelte-kit and the Vite cache
24
+ * survive.
25
+ */
26
+ import { createHash } from 'node:crypto'
27
+ import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
28
+ import { dirname, join, relative, resolve, sep } from 'node:path'
29
+ import { fileURLToPath, pathToFileURL } from 'node:url'
30
+ import { packageDir, readJson } from './util.js'
31
+
32
+ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
33
+ const APP = join(PACKAGE_ROOT, 'app')
34
+ const MARKER = '.reqquest-replace'
35
+ const LOCKFILE = 'reqquest-overrides.lock.json'
36
+ const GENERATED_NOTE = 'generated by `reqquest-ui sync` - do not edit; it is rewritten on every sync'
37
+
38
+ export class SyncError extends Error {}
39
+
40
+ /**
41
+ * @param {string} projectRoot
42
+ * @param {{ strict?: boolean, accept?: boolean, quiet?: boolean }} [opts]
43
+ * strict - fail instead of warn on drift or an out-of-date lockfile (CI / docker builds)
44
+ * accept - re-record the current base hashes in the lockfile, acknowledging any drift
45
+ * @returns {{ appRoot: string, warnings: string[] }}
46
+ */
47
+ export function sync (projectRoot, { strict = false, accept = false, quiet = false } = {}) {
48
+ if (!existsSync(APP)) throw new SyncError(`${APP} is missing - this @reqquest/ui was not packed with its app shell.`)
49
+ const appRoot = join(projectRoot, '.reqquest')
50
+ const pkg = readJson(join(projectRoot, 'package.json'))
51
+ const options = pkg.reqquest ?? {}
52
+ const warnings = []
53
+
54
+ checkToolchain(projectRoot)
55
+ if (!existsSync(join(projectRoot, 'src/local/index.ts')) && !existsSync(join(projectRoot, 'src/local/index.js'))) {
56
+ throw new SyncError('src/local/index.ts not found. It must export `uiRegistry` - see docs/downstream-ui.md in the reqquest repo.')
57
+ }
58
+
59
+ /** @type {Map<string, Buffer>} path relative to appRoot -> contents */
60
+ const files = new Map()
61
+
62
+ // -- src: base shell, then the route overlay
63
+ for (const rel of walk(join(APP, 'src'))) files.set(join('src', rel), readFileSync(join(APP, 'src', rel)))
64
+ const overrides = overlayRoutes(projectRoot, files, options, warnings)
65
+
66
+ // -- static: base, then the project's own
67
+ for (const rel of walk(join(APP, 'static'))) files.set(join('static', rel), readFileSync(join(APP, 'static', rel)))
68
+ for (const rel of walk(join(projectRoot, 'static'))) files.set(join('static', rel), readFileSync(join(projectRoot, 'static', rel)))
69
+
70
+ // -- config stubs and env
71
+ const configModule = pathToFileURL(join(PACKAGE_ROOT, 'cli/config.js')).href
72
+ const configArgs = JSON.stringify({ projectRoot, appRoot, basePath: options.basePath ?? '' })
73
+ files.set('svelte.config.js', Buffer.from(`// ${GENERATED_NOTE}\nimport { svelteConfig } from '${configModule}'\nexport default svelteConfig(${configArgs})\n`))
74
+ files.set('vite.config.js', Buffer.from(`// ${GENERATED_NOTE}\nimport { viteConfig } from '${configModule}'\nexport default viteConfig(${configArgs})\n`))
75
+ files.set('tsconfig.json', Buffer.from(JSON.stringify({
76
+ '//': GENERATED_NOTE,
77
+ extends: './.svelte-kit/tsconfig.json',
78
+ compilerOptions: { sourceMap: true, strictNullChecks: true, skipLibCheck: true }
79
+ }, null, 2) + '\n'))
80
+ // vite-plugin-svelte crawls the root package.json's dependencies to find svelte libraries to
81
+ // prebundle in dev, so list what @reqquest/ui's own package.json would - they resolve upward from here
82
+ const ui = readJson(join(PACKAGE_ROOT, 'package.json'))
83
+ files.set('package.json', Buffer.from(JSON.stringify({
84
+ '//': GENERATED_NOTE,
85
+ name: `${pkg.name ?? 'app'}-reqquest`,
86
+ private: true,
87
+ type: 'module',
88
+ dependencies: { '@reqquest/ui': ui.version, ...ui.dependencies, ...pkg.dependencies },
89
+ devDependencies: ui.peerDependencies
90
+ }, null, 2) + '\n'))
91
+ files.set('.env', mergeEnv(readFileSync(join(APP, 'env.template'), 'utf8'), join(projectRoot, '.env')))
92
+
93
+ reconcile(appRoot, files, ['src', 'static'])
94
+ updateLock(projectRoot, overrides, { strict, accept, warnings })
95
+ ensureRootTsconfig(projectRoot, warnings)
96
+
97
+ if (!quiet) for (const w of warnings) console.warn(`reqquest-ui: ${w}`)
98
+ return { appRoot, warnings }
99
+ }
100
+
101
+ /**
102
+ * The toolchain packages that must be single, top-level copies (svelte, kit, vite, the svelte vite
103
+ * plugin) are exact-pinned peers of @reqquest/ui. npm installs them for the project, but if the
104
+ * project's own package.json declares a different version npm only warns ("ERESOLVE overriding peer
105
+ * dependency") and installs that one. This is the check that actually stops the drift, and every
106
+ * reqquest-ui command runs it.
107
+ */
108
+ function checkToolchain (projectRoot) {
109
+ const { peerDependencies = {}, version } = readJson(join(PACKAGE_ROOT, 'package.json'))
110
+ for (const [name, wanted] of Object.entries(peerDependencies)) {
111
+ let installed
112
+ try {
113
+ installed = readJson(join(packageDir(name, join(projectRoot, '.reqquest')), 'package.json')).version
114
+ } catch {
115
+ throw new SyncError(`${name} is not installed. It is a peer dependency of @reqquest/ui - run npm install.`)
116
+ }
117
+ if (installed !== wanted) {
118
+ throw new SyncError(`@reqquest/ui ${version} requires ${name} ${wanted} but ${installed} is installed. ` +
119
+ `Remove ${name} from your package.json - @reqquest/ui provides it - and reinstall.`)
120
+ }
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Applies the developer's src/routes onto the base routes already in `files`.
126
+ * @returns {Map<string, string>} base route path (relative to routes/) -> hash of the base content
127
+ * the developer has forked, either by overriding the file or by replacing its folder
128
+ */
129
+ function overlayRoutes (projectRoot, files, options, warnings) {
130
+ const routesKey = join('src', 'routes')
131
+ const baseRoutes = new Map()
132
+ for (const [path, content] of files) {
133
+ if (path.startsWith(routesKey + sep)) baseRoutes.set(relative(routesKey, path), content)
134
+ }
135
+ const deleteBase = (prefix) => {
136
+ let found = false
137
+ for (const rel of baseRoutes.keys()) {
138
+ if (rel === prefix || rel.startsWith(prefix + sep)) {
139
+ files.delete(join(routesKey, rel))
140
+ found = true
141
+ }
142
+ }
143
+ return found
144
+ }
145
+ const forked = new Map()
146
+
147
+ for (const route of options.disableRoutes ?? []) {
148
+ const prefix = normalizeRoute(route)
149
+ if (!deleteBase(prefix)) warnings.push(`disableRoutes: "${route}" matches no ReqQuest route - check the spelling.`)
150
+ }
151
+
152
+ const devRoot = join(projectRoot, 'src', 'routes')
153
+ const devFiles = walk(devRoot)
154
+
155
+ for (const rel of devFiles) {
156
+ if (rel.split(sep).pop() !== MARKER) continue
157
+ const prefix = dirname(rel) === '.' ? '' : dirname(rel)
158
+ for (const [baseRel, content] of baseRoutes) {
159
+ if (prefix === '' || baseRel.startsWith(prefix + sep)) forked.set(baseRel, hash(content))
160
+ }
161
+ if (prefix === '') {
162
+ for (const baseRel of baseRoutes.keys()) files.delete(join(routesKey, baseRel))
163
+ } else if (!deleteBase(prefix)) {
164
+ warnings.push(`${join('src/routes', rel)} replaces "${prefix}", which is not a ReqQuest route - the marker does nothing.`)
165
+ }
166
+ }
167
+
168
+ const escapes = []
169
+ for (const rel of devFiles) {
170
+ if (rel.split(sep).pop() === MARKER) continue
171
+ const content = readFileSync(join(devRoot, rel))
172
+ escapes.push(...escapingImports(devRoot, rel, content))
173
+ if (baseRoutes.has(rel)) forked.set(rel, hash(baseRoutes.get(rel)))
174
+ files.set(join(routesKey, rel), content)
175
+ }
176
+ if (escapes.length) {
177
+ throw new SyncError('Route files are copied into .reqquest/ before building, so a relative import that leaves src/routes would break:\n' +
178
+ escapes.map(e => ` ${e}`).join('\n') +
179
+ '\nImport through an alias instead: $lib for src/lib, $local for src/local.')
180
+ }
181
+ return forked
182
+ }
183
+
184
+ const IMPORT_RE = /(?:\bfrom\s*|\bimport\s*\(\s*|\bimport\s+)(['"])(\.{1,2}\/[^'"]*)\1/g
185
+
186
+ function escapingImports (devRoot, rel, content) {
187
+ if (!/\.(svelte|[cm]?[jt]s)$/.test(rel)) return []
188
+ const found = []
189
+ for (const [, , spec] of content.toString('utf8').matchAll(IMPORT_RE)) {
190
+ const target = resolve(devRoot, dirname(rel), spec)
191
+ if (relative(devRoot, target).startsWith('..')) found.push(`${join('src/routes', rel)}: '${spec}'`)
192
+ }
193
+ return found
194
+ }
195
+
196
+ function updateLock (projectRoot, forked, { strict, accept, warnings }) {
197
+ const lockPath = join(projectRoot, LOCKFILE)
198
+ const previous = existsSync(lockPath) ? readJson(lockPath).files ?? {} : {}
199
+ const problems = []
200
+ const next = {}
201
+
202
+ for (const [path, baseHash] of [...forked].sort(([a], [b]) => a.localeCompare(b))) {
203
+ if (!(path in previous)) {
204
+ problems.push(`new override of src/routes/${path} is not recorded`)
205
+ next[path] = baseHash
206
+ } else if (previous[path] !== baseHash && !accept) {
207
+ warnings.push(`ReqQuest changed src/routes/${path} since you overrode it. Compare your copy with ` +
208
+ `node_modules/@reqquest/ui/app/src/routes/${path}, then run \`reqquest-ui sync --accept\`.`)
209
+ problems.push(`src/routes/${path} drifted from its base`)
210
+ next[path] = previous[path] // keep the old hash so the warning repeats until reviewed
211
+ } else {
212
+ next[path] = baseHash
213
+ }
214
+ }
215
+ for (const path of Object.keys(previous)) {
216
+ if (!forked.has(path)) problems.push(`src/routes/${path} is listed but no longer an override`)
217
+ }
218
+
219
+ if (strict && problems.length) {
220
+ throw new SyncError(`${LOCKFILE} is out of date:\n${problems.map(p => ` ${p}`).join('\n')}\n` +
221
+ 'Run `npx reqquest-ui sync` locally (add --accept once you have reviewed any drift) and commit the lockfile.')
222
+ }
223
+ if (JSON.stringify(next) === JSON.stringify(previous)) return
224
+ if (Object.keys(next).length) writeFileSync(lockPath, JSON.stringify({ '//': 'written by `reqquest-ui sync` - commit it', files: next }, null, 2) + '\n')
225
+ else rmSync(lockPath, { force: true })
226
+ }
227
+
228
+ function ensureRootTsconfig (projectRoot, warnings) {
229
+ const path = join(projectRoot, 'tsconfig.json')
230
+ const wanted = './.reqquest/tsconfig.json'
231
+ if (!existsSync(path)) {
232
+ writeFileSync(path, JSON.stringify({ extends: wanted }, null, 2) + '\n')
233
+ } else if (!readFileSync(path, 'utf8').includes(wanted)) {
234
+ warnings.push(`tsconfig.json does not extend "${wanted}", so your editor will not resolve $local, $internal or the route types. Replace it with { "extends": "${wanted}" }.`)
235
+ }
236
+ }
237
+
238
+ /** Template keys first, project .env overriding by key. */
239
+ function mergeEnv (template, projectEnv) {
240
+ const lines = new Map()
241
+ const add = text => {
242
+ for (const line of text.split('\n')) {
243
+ const m = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line)
244
+ if (m) lines.set(m[1], line)
245
+ }
246
+ }
247
+ add(template)
248
+ if (existsSync(projectEnv)) add(readFileSync(projectEnv, 'utf8'))
249
+ return Buffer.from([...lines.values()].join('\n') + '\n')
250
+ }
251
+
252
+ /**
253
+ * Makes appRoot match `files`. Within `managedDirs`, anything not in `files` is deleted; elsewhere
254
+ * (.svelte-kit, node_modules/.vite) is left alone. Generated files are made read-only so an
255
+ * accidental edit fails instead of being silently overwritten by the next sync.
256
+ */
257
+ function reconcile (appRoot, files, managedDirs) {
258
+ for (const dir of managedDirs) {
259
+ for (const rel of walk(join(appRoot, dir))) {
260
+ if (!files.has(join(dir, rel))) rmSync(join(appRoot, dir, rel), { force: true })
261
+ }
262
+ }
263
+ for (const [rel, content] of files) {
264
+ const path = join(appRoot, rel)
265
+ if (existsSync(path) && readFileSync(path).equals(content)) continue
266
+ mkdirSync(dirname(path), { recursive: true })
267
+ if (existsSync(path)) chmodSync(path, 0o644)
268
+ writeFileSync(path, content)
269
+ chmodSync(path, 0o444)
270
+ }
271
+ for (const dir of managedDirs) pruneEmptyDirs(join(appRoot, dir))
272
+ }
273
+
274
+ function pruneEmptyDirs (dir) {
275
+ if (!existsSync(dir)) return
276
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
277
+ if (entry.isDirectory()) pruneEmptyDirs(join(dir, entry.name))
278
+ }
279
+ if (readdirSync(dir).length === 0) rmSync(dir, { recursive: true })
280
+ }
281
+
282
+ /** Relative paths of every file under `root`, or [] if it does not exist. */
283
+ function walk (root) {
284
+ if (!existsSync(root) || !statSync(root).isDirectory()) return []
285
+ return readdirSync(root, { recursive: true, withFileTypes: true })
286
+ .filter(e => e.isFile())
287
+ .map(e => relative(root, join(e.parentPath, e.name)))
288
+ }
289
+
290
+ function normalizeRoute (route) {
291
+ return route.replace(/^\/+|\/+$/g, '').split('/').join(sep)
292
+ }
293
+
294
+ function hash (content) {
295
+ return createHash('sha256').update(content).digest('hex').slice(0, 16)
296
+ }
package/cli/util.js ADDED
@@ -0,0 +1,29 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { dirname, join } from 'node:path'
3
+
4
+ /**
5
+ * Finds an installed package's directory by walking node_modules upward from `from`, the way Node
6
+ * resolves a bare import. Used instead of require.resolve because several of these packages declare
7
+ * "exports" that do not expose package.json.
8
+ */
9
+ export function packageDir (name, from) {
10
+ let dir = from
11
+ while (true) {
12
+ const candidate = join(dir, 'node_modules', name)
13
+ if (existsSync(join(candidate, 'package.json'))) return candidate
14
+ const parent = dirname(dir)
15
+ if (parent === dir) throw new Error(`Cannot find package '${name}' from ${from}. Is @reqquest/ui installed?`)
16
+ dir = parent
17
+ }
18
+ }
19
+
20
+ export function readJson (path) {
21
+ return JSON.parse(readFileSync(path, 'utf8'))
22
+ }
23
+
24
+ /** Absolute path of a package's executable, from its package.json "bin". */
25
+ export function packageBin (name, from, binName = name.split('/').pop()) {
26
+ const dir = packageDir(name, from)
27
+ const { bin } = readJson(join(dir, 'package.json'))
28
+ return join(dir, typeof bin === 'string' ? bin : bin[binName])
29
+ }
@@ -194,6 +194,11 @@ export const uiRegistry = new UIRegistry(config)
194
194
  `'small'` is about 8 lines at 320px; `'large'` about 32 lines at 800px; `'full'` is a full-screen
195
195
  modal, which also **disables autosave in the reviewer UI** and is what supplies `appRequestId` to
196
196
  the reviewer form. All three must stay responsive down to 320px.
197
+ - **`reviewerDialogButtonText`** `string` (default `'Review'`) - a reviewer question registered with
198
+ `formMode: 'full'` gets a labeled button on the reviewer screen that opens its form in the
199
+ full-height side dialog. This is that button's text and the dialog's title; name what the reviewer
200
+ will look at, e.g. `'Review course information'`. Automation prompts and applicant prompts keep the
201
+ edit pencil. The global fallback of the same setting lives on `UIConfig`.
197
202
  - **`displayMode`** `'small' | 'large'` (default `'small'`) - there is deliberately no `'full'`,
198
203
  because the whole prompt list has to print on one page. Use `'large'` together with
199
204
  `PromptDisplayGrid` when one prompt collects several question/answer pairs.
@@ -5271,10 +5276,6 @@ import TagSkeleton from '@txstate-mws/carbon-svelte/dist/TagSkeleton.svelte'
5271
5276
  import UserImpersonation from '@txstate-mws/carbon-svelte/dist/UserImpersonation.svelte'
5272
5277
  ```
5273
5278
 
5274
- `@reqquest/ui` has one of these too: `DelayedSkeleton` is compiled into `dist/components/` but is not
5275
- re-exported from the barrel. The framework's own screens use it via `$lib`; downstream code should
5276
- register a `loader` on the prompt instead of mounting it directly.
5277
-
5278
5279
  ### When this file is not enough
5279
5280
 
5280
5281
  - **Props, exactly.** `carbon-svelte` ships unbundled, so
@@ -93,6 +93,15 @@ export interface PromptDefinition {
93
93
  * Defaults to 'small'.
94
94
  */
95
95
  formMode?: 'small' | 'large' | 'full';
96
+ /**
97
+ * Text of the button on the reviewer screen that opens this prompt's form in the full-height side
98
+ * dialog, also used as that dialog's title. Only applies to reviewer questions registered with
99
+ * `formMode: 'full'`; automation prompts and applicant prompts keep the edit pencil. Name what the
100
+ * reviewer will look at, e.g. "Review course information".
101
+ *
102
+ * Falls back to `UIConfig.reviewerDialogButtonText`, then to "Review".
103
+ */
104
+ reviewerDialogButtonText?: string;
96
105
  /**
97
106
  * Set this to true to make the prompt visually distinct as something that is meant to
98
107
  * be filled in by an automation. If you provide a formComponent, humans with permission
@@ -307,6 +316,13 @@ export interface UIConfig<PK extends string = PromptKey, RK extends string = Req
307
316
  * render nothing anywhere and handle the `statusReasons` prop inside your own form components.
308
317
  */
309
318
  statusReasonNotifications?: StatusReasonNotificationConfig;
319
+ /**
320
+ * Global layer of `PromptDefinition.reviewerDialogButtonText`: the text of the reviewer screen's button
321
+ * that opens a `formMode: 'full'` reviewer question in the full-height side dialog, and that dialog's
322
+ * title. A prompt's own value
323
+ * overrides this one. Defaults to "Review".
324
+ */
325
+ reviewerDialogButtonText?: string;
310
326
  /**
311
327
  * Applicant Review submission page title and subtitle text.
312
328
  */
@@ -434,6 +450,7 @@ export declare class UIRegistry {
434
450
  getWord(key: keyof Terminologies, count?: number, inclusive?: boolean): string;
435
451
  getPlural(key: keyof Terminologies): string;
436
452
  getPrompt(key: string): PromptDefinition | undefined;
453
+ reviewerDialogButtonText(key: string): string;
437
454
  protected statusReasonStatusCache: Map<string, Set<RequirementStatus>>;
438
455
  statusReasonNotificationStatuses(key: string, audience: StatusReasonAudience): Set<RequirementStatus>;
439
456
  getRequirement(key: string): RequirementDefinition | undefined;
package/dist/registry.js CHANGED
@@ -104,6 +104,9 @@ export class UIRegistry {
104
104
  getPrompt(key) {
105
105
  return this.warnIfMissing('prompt', key, this.promptMap[key]);
106
106
  }
107
+ reviewerDialogButtonText(key) {
108
+ return this.promptMap[key]?.reviewerDialogButtonText ?? this.config.reviewerDialogButtonText ?? 'Review';
109
+ }
107
110
  statusReasonStatusCache = new Map();
108
111
  // The statuses whose requirement reasons the framework renders below this prompt's edit form for the given
109
112
  statusReasonNotificationStatuses(key, audience) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reqquest/ui",
3
- "version": "1.17.0",
3
+ "version": "1.19.0",
4
4
  "overrides": {
5
5
  "esrap": "2.3.2"
6
6
  },
@@ -11,8 +11,14 @@
11
11
  "default": "./dist/index.js"
12
12
  }
13
13
  },
14
+ "bin": {
15
+ "reqquest-ui": "bin/reqquest-ui.js"
16
+ },
14
17
  "files": [
15
- "dist"
18
+ "dist",
19
+ "app",
20
+ "bin",
21
+ "cli"
16
22
  ],
17
23
  "scripts": {
18
24
  "prepare": "svelte-kit sync",
@@ -20,40 +26,48 @@
20
26
  "build": "vite build",
21
27
  "preview": "vite preview",
22
28
  "package": "svelte-kit sync && svelte-package",
23
- "prepublishOnly": "npm run package",
24
29
  "check": "svelte-check --tsconfig ./tsconfig.json",
25
30
  "check:registry": "node check-registry.js",
26
31
  "docs:components": "node scripts/build-components-md.js",
27
32
  "docs:components:check": "node scripts/build-components-md.js --check",
28
- "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch"
33
+ "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
34
+ "prepack": "npm run package && node scripts/pack-app.js"
29
35
  },
30
36
  "peerDependencies": {
31
- "@sveltejs/kit": "^2.0.0",
32
- "svelte": "^5.0.0"
37
+ "@sveltejs/kit": "2.70.3",
38
+ "@sveltejs/vite-plugin-svelte": "6.2.4",
39
+ "svelte": "5.57.2",
40
+ "vite": "7.3.7"
33
41
  },
34
42
  "dependencies": {
43
+ "@sveltejs/adapter-static": "3.0.10",
35
44
  "@txstate-mws/carbon-svelte": "^1.5.5",
36
45
  "@txstate-mws/fastify-shared": "^1.1.0",
37
46
  "@txstate-mws/svelte-components": "^1.6.11",
47
+ "@txstate-mws/svelte-forms": "2.1.10",
38
48
  "@txstate-mws/sveltekit-utils": "^1.0.2",
49
+ "autoprefixer": "10.6.1",
50
+ "carbon-components-svelte": "0.112.2",
51
+ "carbon-icons-svelte": "13.16.0",
52
+ "carbon-preprocess-svelte": "0.11.49",
39
53
  "luxon": "^3.5.0",
40
- "txstate-utils": "^1.8.15"
54
+ "postcss": "8.5.29",
55
+ "svelte-check": "4.7.6",
56
+ "tailwindcss": "3.4.19",
57
+ "txstate-utils": "^1.8.15",
58
+ "typescript": "5.9.3"
41
59
  },
42
60
  "devDependencies": {
43
61
  "@stylistic/eslint-plugin": "^5.0.0",
44
- "@sveltejs/adapter-static": "^3.0.1",
45
- "@sveltejs/kit": "^2.5.2",
62
+ "@sveltejs/kit": "2.70.3",
46
63
  "@sveltejs/package": "^2.0.0",
47
- "@sveltejs/vite-plugin-svelte": "^6.0.0",
64
+ "@sveltejs/vite-plugin-svelte": "6.2.4",
48
65
  "@types/luxon": "^3.4.2",
49
66
  "@types/node": "^24.3.1",
50
- "carbon-preprocess-svelte": "~0.11.2",
51
67
  "eslint-config-love": "^151.0.0",
52
68
  "eslint-plugin-svelte": "^3.0.0",
53
- "svelte": "^5.0.0",
54
- "svelte-check": "^4.0.0",
55
- "typescript": "^5.3.3",
56
- "vite": "^7.0.0"
69
+ "svelte": "5.57.2",
70
+ "vite": "7.3.7"
57
71
  },
58
72
  "type": "module"
59
73
  }
@@ -1,21 +0,0 @@
1
- import type { Loader } from "../registry";
2
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
3
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
4
- $$bindings?: Bindings;
5
- } & Exports;
6
- (internal: unknown, props: Props & {
7
- $$events?: Events;
8
- $$slots?: Slots;
9
- }): Exports & {
10
- $set?: any;
11
- $on?: any;
12
- };
13
- z_$$bindings?: Bindings;
14
- }
15
- declare const DelayedSkeleton: $$__sveltets_2_IsomorphicComponent<{
16
- loader: Loader;
17
- }, {
18
- [evt: string]: CustomEvent<any>;
19
- }, {}, {}, string>;
20
- type DelayedSkeleton = InstanceType<typeof DelayedSkeleton>;
21
- export default DelayedSkeleton;