@adcops/autocore-react 3.3.59 → 3.3.63

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 (53) hide show
  1. package/dist/components/ams/AmsProvider.d.ts +45 -0
  2. package/dist/components/ams/AmsProvider.d.ts.map +1 -0
  3. package/dist/components/ams/AmsProvider.js +1 -0
  4. package/dist/components/ams/AssetDetailView.d.ts +3 -0
  5. package/dist/components/ams/AssetDetailView.d.ts.map +1 -0
  6. package/dist/components/ams/AssetDetailView.js +1 -0
  7. package/dist/components/ams/AssetRegistryTable.d.ts +3 -0
  8. package/dist/components/ams/AssetRegistryTable.d.ts.map +1 -0
  9. package/dist/components/ams/AssetRegistryTable.js +1 -0
  10. package/dist/components/ams/CalibrationEntryDialog.d.ts +10 -0
  11. package/dist/components/ams/CalibrationEntryDialog.d.ts.map +1 -0
  12. package/dist/components/ams/CalibrationEntryDialog.js +1 -0
  13. package/dist/components/ams/SubLocationPicker.d.ts +3 -0
  14. package/dist/components/ams/SubLocationPicker.d.ts.map +1 -0
  15. package/dist/components/ams/SubLocationPicker.js +1 -0
  16. package/dist/components/ams/index.d.ts +6 -0
  17. package/dist/components/ams/index.d.ts.map +1 -0
  18. package/dist/components/ams/index.js +1 -0
  19. package/dist/components/index.d.ts +9 -0
  20. package/dist/components/index.d.ts.map +1 -1
  21. package/dist/components/index.js +1 -1
  22. package/dist/components/tis/ProjectSelector.d.ts +15 -0
  23. package/dist/components/tis/ProjectSelector.d.ts.map +1 -0
  24. package/dist/components/tis/ProjectSelector.js +1 -0
  25. package/dist/components/tis/TestDataView.d.ts +9 -1
  26. package/dist/components/tis/TestDataView.d.ts.map +1 -1
  27. package/dist/components/tis/TestDataView.js +1 -1
  28. package/dist/components/tis/TestSetupForm.d.ts +8 -4
  29. package/dist/components/tis/TestSetupForm.d.ts.map +1 -1
  30. package/dist/components/tis/TestSetupForm.js +1 -1
  31. package/dist/components/tis/TisProvider.d.ts +45 -0
  32. package/dist/components/tis/TisProvider.d.ts.map +1 -1
  33. package/dist/components/tis/TisProvider.js +1 -1
  34. package/dist/core/AutoCoreTagContext.d.ts +16 -0
  35. package/dist/core/AutoCoreTagContext.d.ts.map +1 -1
  36. package/dist/core/AutoCoreTagContext.js +1 -1
  37. package/dist/themes/adc-dark/blue/theme.css +67 -37
  38. package/dist/themes/adc-dark/blue/theme.css.map +1 -1
  39. package/package.json +1 -1
  40. package/src/components/ams/AmsProvider.tsx +219 -0
  41. package/src/components/ams/AssetDetailView.tsx +101 -0
  42. package/src/components/ams/AssetRegistryTable.tsx +171 -0
  43. package/src/components/ams/CalibrationEntryDialog.tsx +197 -0
  44. package/src/components/ams/SubLocationPicker.tsx +146 -0
  45. package/src/components/ams/index.ts +12 -0
  46. package/src/components/index.ts +30 -0
  47. package/src/components/tis/ProjectSelector.tsx +190 -0
  48. package/src/components/tis/TestDataView.tsx +321 -28
  49. package/src/components/tis/TestSetupForm.tsx +66 -253
  50. package/src/components/tis/TisProvider.tsx +192 -1
  51. package/src/core/AutoCoreTagContext.tsx +114 -16
  52. package/src/themes/adc-dark/_extensions.scss +15 -0
  53. package/src/themes/adc-dark/blue/adc_theme.scss +56 -10
@@ -90,6 +90,45 @@ export interface TisContextValue {
90
90
  selection: TisSelection;
91
91
  setSelection: (patch: TisSelectionPatch) => void;
92
92
 
93
+ // -----------------------------------------------------------------
94
+ // Project management — used by both <ProjectSelector> (Project tab)
95
+ // and <TestSetupForm> (Test tab) so they share a single source of
96
+ // truth for "which projects are real" and "what fields does the
97
+ // current one have." Keeping this in the provider rather than in
98
+ // TestSetupForm lets the two components live in different tabs
99
+ // without prop-threading.
100
+ // -----------------------------------------------------------------
101
+
102
+ /** Project IDs returned by the server's `tis.list_projects`. */
103
+ existingProjects: string[];
104
+ /** True when the project either exists on disk OR was created in
105
+ * this browser session via `<ProjectInfoDialog mode="create">`.
106
+ * This is the gate for staging — typing an unknown name is
107
+ * invalid until + creates the directory. */
108
+ projectKnown: (id: string) => boolean;
109
+ /** Refresh `existingProjects` from the server. Called automatically
110
+ * on `tis.project_created` / `tis.project_updated` broadcasts. */
111
+ refreshProjects: () => Promise<void>;
112
+ /** Add a project ID to the in-session "just created" set so the
113
+ * form is immediately valid for it without round-tripping to
114
+ * list_projects. Idempotent. */
115
+ markProjectJustCreated: (id: string) => void;
116
+
117
+ /** `project_fields` blob for the currently-selected project,
118
+ * fetched from project.json. `{}` when nothing is loaded yet
119
+ * (use `projectFieldsLoaded` to disambiguate "empty project" vs
120
+ * "still fetching"). */
121
+ projectFields: Record<string, any>;
122
+ projectFieldsLoaded: boolean;
123
+ /** Fetch and cache project_fields for one project. Returns the
124
+ * fields on success, or null on error. The current selection's
125
+ * fields are also re-loaded automatically when `selection.projectId`
126
+ * changes. */
127
+ loadProjectFields: (id: string) => Promise<Record<string, any> | null>;
128
+ /** Stash freshly-known project_fields without a round trip — used
129
+ * by the create / edit dialogs after a successful submit. */
130
+ setProjectFields: (id: string, fields: Record<string, any>) => void;
131
+
93
132
  /** Fetch the run list for a (project, method?) pair. Method may be
94
133
  * omitted to aggregate runs across every method in the project —
95
134
  * the History tab uses this. */
@@ -120,6 +159,14 @@ const TisContext = createContext<TisContextValue>({
120
159
  state: EMPTY_STATE,
121
160
  selection: EMPTY_SELECTION,
122
161
  setSelection: () => {},
162
+ existingProjects: [],
163
+ projectKnown: () => false,
164
+ refreshProjects: async () => {},
165
+ markProjectJustCreated: () => {},
166
+ projectFields: {},
167
+ projectFieldsLoaded: false,
168
+ loadProjectFields: async () => null,
169
+ setProjectFields: () => {},
123
170
  fetchRuns: async () => [],
124
171
  fetchRun: async () => null,
125
172
  runCache: {},
@@ -294,6 +341,33 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
294
341
  }));
295
342
  }, []);
296
343
 
344
+ // -----------------------------------------------------------------
345
+ // Auto-clear pins on a new active run.
346
+ //
347
+ // The server broadcasts `tis.active_run_id` immediately at
348
+ // `start_test` (before any cycle is added). When that scalar
349
+ // transitions to a new, non-empty value, any pins the operator
350
+ // set — typically by clicking a row in <ResultHistoryTable> —
351
+ // would otherwise keep <TestDataView> stuck on the old run. We
352
+ // drop all four pins here so `selection` falls through to the
353
+ // active broadcast scalars and the Data view follows the live
354
+ // test automatically.
355
+ //
356
+ // Pins set *after* the new run begins (e.g., the operator opens
357
+ // History mid-test and clicks a historical row) are honoured —
358
+ // they're set after this effect runs and they win until the
359
+ // next Start. End-of-test (active flips false) doesn't clear
360
+ // either: active_run_id keeps the just-finished run's value, so
361
+ // the operator can still read its results.
362
+ const prevActiveRunRef = useRef('');
363
+ useEffect(() => {
364
+ const newRunId = state.activeRunId;
365
+ if (newRunId && newRunId !== prevActiveRunRef.current) {
366
+ prevActiveRunRef.current = newRunId;
367
+ setPins({ projectId: null, methodId: null, sampleId: null, runId: null });
368
+ }
369
+ }, [state.activeRunId]);
370
+
297
371
  // -----------------------------------------------------------------
298
372
  // Fetchers
299
373
  // -----------------------------------------------------------------
@@ -340,11 +414,111 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
340
414
 
341
415
  const runCache = useMemo(() => ({ ...cacheRef.current }), [cacheVersion]);
342
416
 
417
+ // -----------------------------------------------------------------
418
+ // Project management state
419
+ //
420
+ // Mirrors what TestSetupForm used to track locally, but lifted up
421
+ // here so <ProjectSelector> on the Project tab and <TestSetupForm>
422
+ // on the Test tab share a single source of truth. Without this
423
+ // lift, the two tabs would each fire their own list_projects and
424
+ // disagree on which IDs are valid.
425
+ // -----------------------------------------------------------------
426
+ const [existingProjects, setExistingProjects] = useState<string[]>([]);
427
+ const justCreatedRef = useRef<Set<string>>(new Set());
428
+ const [projectsTick, setProjectsTick] = useState(0); // bumps on Set mutation
429
+ const [projectFieldsCache, setProjectFieldsCache] = useState<Record<string, Record<string, any>>>({});
430
+
431
+ const refreshProjects = useCallback(async () => {
432
+ try {
433
+ const resp: any = await invoke('tis.list_projects' as any, MessageType.Request, {} as any);
434
+ if (resp?.success && resp.data?.projects) {
435
+ setExistingProjects(resp.data.projects as string[]);
436
+ }
437
+ } catch (e) {
438
+ console.error('[TisProvider] tis.list_projects failed:', e);
439
+ }
440
+ }, [invoke]);
441
+
442
+ const markProjectJustCreated = useCallback((id: string) => {
443
+ if (!id) return;
444
+ if (justCreatedRef.current.has(id)) return;
445
+ justCreatedRef.current.add(id);
446
+ setProjectsTick(t => t + 1);
447
+ }, []);
448
+
449
+ const projectKnown = useCallback((id: string) => {
450
+ if (!id) return false;
451
+ if (justCreatedRef.current.has(id)) return true;
452
+ return existingProjects.includes(id);
453
+ // existingProjects + projectsTick are deps but useCallback
454
+ // closes over them; consumers read current values fine.
455
+ // eslint-disable-next-line react-hooks/exhaustive-deps
456
+ }, [existingProjects, projectsTick]);
457
+
458
+ const setProjectFields = useCallback((id: string, fields: Record<string, any>) => {
459
+ if (!id) return;
460
+ setProjectFieldsCache(prev => ({ ...prev, [id]: fields }));
461
+ }, []);
462
+
463
+ const loadProjectFields = useCallback(async (id: string): Promise<Record<string, any> | null> => {
464
+ if (!id) return null;
465
+ try {
466
+ const resp: any = await invoke('tis.read_project' as any, MessageType.Request, { project_id: id } as any);
467
+ if (resp?.success) {
468
+ const fields = (resp.data?.project_fields ?? {}) as Record<string, any>;
469
+ setProjectFieldsCache(prev => ({ ...prev, [id]: fields }));
470
+ return fields;
471
+ }
472
+ } catch (e) {
473
+ console.warn('[TisProvider] tis.read_project failed:', e);
474
+ }
475
+ return null;
476
+ }, [invoke]);
477
+
478
+ // Initial project list load + refresh on server-side mutation
479
+ // broadcasts. Mark-just-created is purely local; the server
480
+ // broadcasts kick the persisted list back into sync if a project
481
+ // was added by another client (or the next time `acctl` writes a
482
+ // new directory).
483
+ useEffect(() => { void refreshProjects(); }, [refreshProjects]);
484
+
485
+ useEffect(() => {
486
+ const onCreated = () => { void refreshProjects(); };
487
+ const onUpdated = (payload: any) => {
488
+ const pid = typeof payload?.project_id === 'string' ? payload.project_id : '';
489
+ if (pid) void loadProjectFields(pid);
490
+ };
491
+ const id1 = subscribe('tis.project_created', onCreated);
492
+ const id2 = subscribe('tis.project_updated', onUpdated);
493
+ return () => { unsubscribe(id1); unsubscribe(id2); };
494
+ }, [subscribe, unsubscribe, refreshProjects, loadProjectFields]);
495
+
496
+ // Auto-fetch project_fields whenever the selection lands on a
497
+ // known project we don't yet have cached. The Test tab's stage
498
+ // payload depends on this.
499
+ useEffect(() => {
500
+ const pid = selection.projectId;
501
+ if (!pid || !projectKnown(pid)) return;
502
+ if (projectFieldsCache[pid] !== undefined) return;
503
+ void loadProjectFields(pid);
504
+ }, [selection.projectId, projectKnown, projectFieldsCache, loadProjectFields]);
505
+
506
+ const projectFields = projectFieldsCache[selection.projectId] ?? {};
507
+ const projectFieldsLoaded = projectFieldsCache[selection.projectId] !== undefined;
508
+
343
509
  const value: TisContextValue = useMemo(() => ({
344
510
  schemas, defaultMethodId, schemasLoaded,
345
511
  state, selection, setSelection,
512
+ existingProjects, projectKnown, refreshProjects, markProjectJustCreated,
513
+ projectFields, projectFieldsLoaded, loadProjectFields, setProjectFields,
346
514
  fetchRuns, fetchRun, runCache,
347
- }), [schemas, defaultMethodId, schemasLoaded, state, selection, setSelection, fetchRuns, fetchRun, runCache]);
515
+ }), [
516
+ schemas, defaultMethodId, schemasLoaded,
517
+ state, selection, setSelection,
518
+ existingProjects, projectKnown, refreshProjects, markProjectJustCreated,
519
+ projectFields, projectFieldsLoaded, loadProjectFields, setProjectFields,
520
+ fetchRuns, fetchRun, runCache,
521
+ ]);
348
522
 
349
523
  return <TisContext.Provider value={value}>{children}</TisContext.Provider>;
350
524
  };
@@ -356,6 +530,23 @@ export const TisProvider: React.FC<TisProviderProps> = ({ children, defaultMetho
356
530
  export const useTis = () => useContext(TisContext);
357
531
  export const useTisSchemas = () => useContext(TisContext).schemas;
358
532
  export const useTisState = () => useContext(TisContext).state;
533
+ /**
534
+ * Tuple of `[selection, setSelection]`.
535
+ *
536
+ * `selection` has four fields — `projectId`, `methodId`, `sampleId`,
537
+ * `runId`. Each one falls through to the matching `tis.active_*`
538
+ * broadcast scalar when no pin is set. Pins are set by passing a
539
+ * concrete value to `setSelection`; pass `null` to clear a pin and
540
+ * resume auto-following.
541
+ *
542
+ * **Auto-clear on Start:** when a new test becomes active (the
543
+ * `tis.active_run_id` scalar transitions to a new non-empty value),
544
+ * the provider drops all four pins automatically. This is what makes
545
+ * `<TestDataView>` follow the live run after an operator has been
546
+ * browsing history — they don't have to click anything to "switch
547
+ * to the live test." Pins set *after* the new run begins (mid-test
548
+ * History click) are honoured until the next Start.
549
+ */
359
550
  export const useTisSelection = () => {
360
551
  const { selection, setSelection } = useContext(TisContext);
361
552
  return [selection, setSelection] as const;
@@ -151,13 +151,29 @@ const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
151
151
  * @param props.tags - List of tag configurations (tagName, fqdn, etc.)
152
152
  * @param props.scales - Map of scale definitions (name, factor, label)
153
153
  * @param props.eagerRead - If true, automatically fetches initial values on mount
154
+ * @param props.flushIntervalMs - Maximum rate at which broadcast updates trigger
155
+ * React state changes. Broadcasts arriving faster than this are coalesced
156
+ * per-tag (latest-value-wins) and flushed in a single batched render at most
157
+ * once per interval.
158
+ *
159
+ * Default `33` (≈30 FPS) keeps Firefox smooth even when the controller scan
160
+ * rate is 2–4 kHz. The wire still carries every change-of-value broadcast —
161
+ * this only paces the React reconciliation cost, which dominates render
162
+ * budget. To also reduce wire bandwidth (relevant on poor links / Tailscale
163
+ * over slow WiFi), see the connection-level rate cap (server-side).
164
+ *
165
+ * Set to `0` to disable throttling and apply every broadcast immediately
166
+ * (matches pre-throttle behaviour). Initial-load `eagerRead` values are
167
+ * never throttled — they bypass this path entirely so the page renders
168
+ * fully populated on mount.
154
169
  */
155
170
  export const AutoCoreTagProvider: React.FC<{
156
171
  children: ReactNode;
157
172
  tags: readonly TagConfig[];
158
173
  scales?: Record<string, ScaleConfig>;
159
174
  eagerRead?: boolean;
160
- }> = ({ children, tags, scales, eagerRead = true }) => {
175
+ flushIntervalMs?: number;
176
+ }> = ({ children, tags, scales, eagerRead = true, flushIntervalMs = 33 }) => {
161
177
  const startedRef = useRef(false);
162
178
 
163
179
  // PERFORMANCE: Memoize default scales to ensure reference stability.
@@ -281,25 +297,107 @@ export const AutoCoreTagProvider: React.FC<{
281
297
  });
282
298
  }, [tags, toDisplay]);
283
299
 
300
+ // -----------------------------------------------------------------
301
+ // Throttle: broadcast updates are coalesced per tag and flushed in
302
+ // a single batched setState at most once per `flushIntervalMs`. This
303
+ // exists because every broadcast that lands in `setRawValues` triggers
304
+ // a context-wide React reconciliation. With high-rate scan loops
305
+ // (3830 runs at 4 kHz) and ~10 fast-changing tags, that's tens of
306
+ // thousands of reconciliations per second — Firefox in particular
307
+ // stalls visibly. The batched flush keeps reconciliation at the
308
+ // `flushIntervalMs` rate (~30 FPS by default), regardless of how
309
+ // many broadcasts the wire delivers in between.
310
+ //
311
+ // Latest-value-wins: each tag keeps only its most recent raw + display
312
+ // value in the pending maps; intermediate values within a window are
313
+ // dropped. This is the right semantic for status indicators — the
314
+ // operator wants "where is X right now," not the path it took to get
315
+ // there. Event broadcasts (e.g., tis.cycle_added) don't go through
316
+ // this path; they're consumed by their own subscribers.
317
+ //
318
+ // The eagerRead path on mount writes directly to setRawValues (see
319
+ // `eagerPullNonADS` etc. above) and bypasses this throttle, so the
320
+ // initial render is fully populated.
321
+ // -----------------------------------------------------------------
322
+ const pendingRawRef = useRef<Map<string, unknown>>(new Map());
323
+ const pendingDisplayRef = useRef<Map<string, unknown>>(new Map());
324
+ const flushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
325
+
326
+ const flushPending = useCallback(() => {
327
+ flushTimerRef.current = null;
328
+ const r = pendingRawRef.current;
329
+ const d = pendingDisplayRef.current;
330
+ if (r.size === 0 && d.size === 0) return;
331
+ pendingRawRef.current = new Map();
332
+ pendingDisplayRef.current = new Map();
333
+ setRawValues(prev => {
334
+ let next = prev;
335
+ r.forEach((v, k) => {
336
+ if (next[k] !== v) {
337
+ if (next === prev) next = { ...prev };
338
+ next[k] = v;
339
+ }
340
+ });
341
+ return next;
342
+ });
343
+ setValues(prev => {
344
+ let next = prev;
345
+ d.forEach((v, k) => {
346
+ if (next[k] !== v) {
347
+ if (next === prev) next = { ...prev };
348
+ next[k] = v;
349
+ }
350
+ });
351
+ return next;
352
+ });
353
+ }, []);
354
+
284
355
  /**
285
- * Handles incoming value updates from the server.
286
- *
287
- * 1. Updates `rawValues` (Source of Truth).
288
- * 2. Computes and updates `values` (Display).
289
- * 3. Uses functional state updates to ensure atomicity.
290
- * 4. Keys by `tagName` for consistent access by hooks.
356
+ * Handles incoming broadcast value updates from the server.
357
+ *
358
+ * 1. Stages the raw and display values in per-tag pending maps
359
+ * (latest-value-wins; intermediate values inside a window are
360
+ * dropped).
361
+ * 2. Schedules (or piggybacks on) a flush timer that batches every
362
+ * accumulated tag into a single setRawValues + setValues pass at
363
+ * most once per `flushIntervalMs`.
364
+ * 3. When `flushIntervalMs === 0`, flushes synchronously — same as
365
+ * the pre-throttle path.
291
366
  */
292
367
  const handleTagUpdate = useCallback((tag: TagConfig, raw: unknown) => {
293
- // Store the raw controller value (source of truth for scaling)
294
- setRawValues(prev =>
295
- prev[tag.tagName] === raw ? prev : { ...prev, [tag.tagName]: raw }
296
- );
297
- // Compute and store the display value (with scaling/codecs applied)
298
368
  const display = toDisplay(tag, raw);
299
- setValues(prev =>
300
- prev[tag.tagName] === display ? prev : { ...prev, [tag.tagName]: display }
301
- );
302
- }, [toDisplay]);
369
+
370
+ if (flushIntervalMs <= 0) {
371
+ // Throttle disabled — preserve the original synchronous shape.
372
+ setRawValues(prev =>
373
+ prev[tag.tagName] === raw ? prev : { ...prev, [tag.tagName]: raw }
374
+ );
375
+ setValues(prev =>
376
+ prev[tag.tagName] === display ? prev : { ...prev, [tag.tagName]: display }
377
+ );
378
+ return;
379
+ }
380
+
381
+ pendingRawRef.current.set(tag.tagName, raw);
382
+ pendingDisplayRef.current.set(tag.tagName, display);
383
+ if (flushTimerRef.current === null) {
384
+ flushTimerRef.current = setTimeout(flushPending, flushIntervalMs);
385
+ }
386
+ }, [toDisplay, flushIntervalMs, flushPending]);
387
+
388
+ // Drain any pending updates on unmount so we don't leak the timer
389
+ // reference and so a late-arriving setState doesn't fire on a
390
+ // dead component during dev-mode StrictMode double-mounts.
391
+ useEffect(() => {
392
+ return () => {
393
+ if (flushTimerRef.current !== null) {
394
+ clearTimeout(flushTimerRef.current);
395
+ flushTimerRef.current = null;
396
+ }
397
+ pendingRawRef.current = new Map();
398
+ pendingDisplayRef.current = new Map();
399
+ };
400
+ }, []);
303
401
 
304
402
  /**
305
403
  * Eagerly fetches initial values for non-ADS domains (e.g., MODBUS).
@@ -175,6 +175,21 @@
175
175
  gap: 2px;
176
176
  }
177
177
 
178
+ /* ---- ac-toolbar-tool-list: stacked text buttons in an OverlayPanel ---- */
179
+
180
+ .ac-toolbar-tool-list {
181
+ display: flex;
182
+ flex-direction: column;
183
+ min-width: 16rem;
184
+ padding: 2mm;
185
+ }
186
+
187
+ .ac-toolbar-tool-item {
188
+ justify-content: flex-start;
189
+ gap: 0.75rem;
190
+ margin: 2mm;
191
+ }
192
+
178
193
  /* ---- ac-form: content page form layout ---- */
179
194
 
180
195
  .ac-form {
@@ -14,8 +14,15 @@
14
14
  // General Padding & Sizing
15
15
  $panelContentPadding: 0.5rem 0.75rem !default; // Affects panels, cards, dialogs, etc.
16
16
  $inputPadding: 0.5rem 0.75rem !default; // Affects all input fields
17
- $buttonPadding: 0.5rem 0.75rem !default; // Affects buttons
18
- $buttonIconOnlyPadding: 0.5rem 0 !default;
17
+ // Buttons run one size class larger than the original lara baseline:
18
+ // the previous "medium" was too small for our 1080p kiosk targets and
19
+ // the workstation operators leaning on touchscreens. Each of small /
20
+ // medium / large was bumped up one tier (+0.125rem on font and on each
21
+ // padding axis), so the new "small" is the size the old "medium" was.
22
+ // `.p-button-sm` and `.p-button-lg` overrides below carry the matching
23
+ // numbers for the other two tiers.
24
+ $buttonPadding: 0.625rem 0.875rem !default; // Affects buttons (medium/default)
25
+ $buttonIconOnlyPadding: 0.625rem 0 !default;
19
26
  $inlineSpacing: 0.25rem !default; // Space between items like icons and text
20
27
  $borderRadius: 3px !default; // Sharper corners for a more "desktop" feel
21
28
 
@@ -24,8 +31,16 @@ $tableHeaderCellPadding: 0.5rem 0.75rem !default;
24
31
  $tableBodyCellPadding: 0.5rem 0.75rem !default;
25
32
 
26
33
  // Menu Sizing
27
- $menuitemPadding: 0.5rem 0.75rem !default;
28
- $horizontalMenuRootMenuitemPadding: 0.5rem 0.75rem !default;
34
+ //
35
+ // Bumped past the medium-button padding because menu items are
36
+ // usually selected in a single tap on a touchscreen with imprecise
37
+ // aim — slightly more vertical breathing room than a button gives a
38
+ // touch target close to the 44 px iOS-HIG / WCAG 2.5.5 minimum at
39
+ // our 1.125rem (~18 px) menu-item font-size: 18 + 2×12 = 42 px.
40
+ // `.p-menuitem-link` font-size and `.p-menuitem-icon` size below
41
+ // finish the picture (icons scale up alongside the text).
42
+ $menuitemPadding: 0.75rem 1rem !default;
43
+ $horizontalMenuRootMenuitemPadding: 0.75rem 1rem !default;
29
44
 
30
45
 
31
46
  // 2. Import the original lara-dark-blue theme
@@ -49,7 +64,7 @@ textarea {
49
64
 
50
65
  .p-button {
51
66
  padding: $buttonPadding;
52
- font-size: 1rem;
67
+ font-size: 1.125rem;
53
68
  font-family: inherit;
54
69
  border-radius: $borderRadius;
55
70
  cursor: pointer;
@@ -61,7 +76,7 @@ textarea {
61
76
 
62
77
  .p-button.p-button-icon-only {
63
78
  padding: $buttonIconOnlyPadding;
64
- width: 2.5rem;
79
+ width: 2.75rem;
65
80
  }
66
81
 
67
82
  // PrimeReact always renders a `<span class="p-button-label">&nbsp;</span>`
@@ -72,14 +87,16 @@ textarea {
72
87
  display: none;
73
88
  }
74
89
 
90
+ // `.p-button-sm` lands at what used to be the medium baseline.
75
91
  .p-button.p-button-sm {
76
- font-size: 0.875rem;
77
- padding: 0.375rem 0.625rem;
92
+ font-size: 1rem;
93
+ padding: 0.5rem 0.75rem;
78
94
  }
79
95
 
96
+ // `.p-button-lg` extends the same +0.125rem step past the new medium.
80
97
  .p-button.p-button-lg {
81
- font-size: 1.125rem;
82
- padding: 0.625rem 0.875rem;
98
+ font-size: 1.25rem;
99
+ padding: 0.75rem 1rem;
83
100
  }
84
101
 
85
102
  .p-inputtext {
@@ -133,6 +150,35 @@ textarea {
133
150
  border-bottom-right-radius: $borderRadius;
134
151
  }
135
152
 
153
+ // Menu items (TieredMenu / Menu / SplitButton dropdown / ContextMenu).
154
+ // Match the new medium-button font-size so SplitButton dropdowns don't
155
+ // look noticeably smaller than the button that opened them, and bump
156
+ // the leading icon a touch larger than the text so radio/check glyphs
157
+ // read clearly on a touchscreen.
158
+ .p-menuitem-link {
159
+ font-size: 1.125rem;
160
+ }
161
+ .p-menuitem-link .p-menuitem-icon {
162
+ font-size: 1.25rem;
163
+ width: 1.25rem;
164
+ height: 1.25rem;
165
+ }
166
+
167
+ // Input-list popup items (Dropdown / AutoComplete / MultiSelect /
168
+ // CascadeSelect / TreeSelect / picklist filters). Vertical padding
169
+ // for these is already at the touch-friendly lara default
170
+ // (`$inputListItemPadding: 0.75rem 1.25rem`); we just bump the
171
+ // font-size to the medium-button scale so options read at the same
172
+ // visual weight as the button / menu items around them. Net target
173
+ // height ≈ 18 px font + 24 px padding = 42 px, same as the menus.
174
+ .p-dropdown-item,
175
+ .p-autocomplete-item,
176
+ .p-multiselect-item,
177
+ .p-cascadeselect-item,
178
+ .p-treeselect-item {
179
+ font-size: 1.125rem;
180
+ }
181
+
136
182
  // TabView: ensure tabs are visibly tab-shaped with padding and spacing,
137
183
  // regardless of @layer cascade resolution.
138
184
  .p-tabview .p-tabview-nav {