pebble-docs

PebbleUI Docs

Overview
PebbleSRV

About PebbleUI

# PebbleUI is Spacefix’s proprietory frontend application framework — a batteries-included, opinionated client runtime for building and scaling modern React applications.

# It goes beyond a UI kit by standardizing how applications are structured, configured, themed, and composed. PebbleUI provides a single, well-maintained foundation that bundles navigation, data-fetching, forms, layouts, rich content, and enhanced Material UI primitives into a cohesive runtime teams can reuse across products.

# By centralizing architectural patterns and complex integrations once, PebbleUI reduces duplication, improves consistency, and allows teams to focus on product features instead of reinventing frontend infrastructure.

Maintenance Guide

# Below is the Modules or folders Heirarchy (Descending order - general to specific)
# Generally, Modules or folders should not be imported into Modules above them in the list.

# NOTE:  The main reason for the Modules Heirarchy is to ensure modularity, code organization and to ultimately avoid "Bidirectional Dependency" and "Circular Dependency" issues. However, as a tradeoff to keep codes D.R.Y., we, when and only when most necessary (and most importantly when it has been confirmed that it can never hurt the app or cause any potential runtime errors) violate the Modules Heirarchy (still, only at the folders level, NEVER at the files level).
# E.g., 1: importing DraftsList into mui-data-grid folder from form folder while some files in the form folder imports some items (like GridActions) from the mui-data-grid folder.
# E.g., 2: importing TextInput, TextInputFallback into entity-viewers from form folder while form folder imports ListLookup

# config

# api-clients

# assets

# entities
# - interface files heirarchy of the entities folder: domains.entities.ts, .entity.ts, entities.interface.ts.
# - this is a guide for all similar nested interface files. A similar stuff in form folder.

# helpers

# auth

# theme

# components
# - core copmonents hierarchy: rich-text-editor, data-grid, entity-viewers, form (note exeption is in taking the DraftTable to data-grid folder from form folder but since it's not a clash between exact file for file, we won't have bicircular dependency issue with that).
# - the helpers folder inside of components folder can be imported into any of the other components
# - heirarchy of the entity-viewers: autocompletes, look-ups, tab-viewer, row-viewer, ViewerLayout

# pages
# - pages hierarchy for dashboard administration - settings, then the others)

# routing

# layout

# PebbleApp (equivalent of App)

# PebbleAppProvider (equivalent of main.tsx)

# index.tsx

#--
# The utils, enums, constants, interfaces etc needed by BOTH entities folder and the rest of the app are in the entities folder
# The lib folder inside entities folder contains all the items needed by ONLY the rest of the app

# NOTE: For custom build folders => build utils, scripts

General Notes

# Avoid calling react-toastify directly in rendering logic. Since it performs side effects, call it only in event handlers or useEffects.
# React states should only be updated in use effects or in event handlers.
# Whenever the toast() is called inside the catch() of a failed server request, always toast thus toast('error', handleServerError(error)). This way we can have a central place to access server errors (for logging or other needs, although most of our error logging and monitoring have been handled on the backend). This function is also used anywhere server errors are likely to gotten.
# All pagination rows per page options must be kept at a maximum of 50 (50 items or 50 page size). Anything above this will cause performance overhead on the DataBase.

ReactQuery

# Note: We are using react-query only for key fetches such as the grid fetches, viewer fetches, enum fetches, and other fetches where caching is mandatory and not all async ops in the app.

# -- ERROR MANAGMENT
# We configured global error handling for all react-query in the main.ts file such that when an error occurs while there is data in the cache (which indicates a failed background update), we simply toast that error. There we can actually perform a side effect, thus our toast! (even tho a toast is the most fitting way to display a failed background error).
# If necessary consider modifying the error message passed to the toast call to indicate that it is a failed background update.

# For all places where react-query will be consumed, to manage error locally (which is what we should do as long as the error is not a failed background update), either update an error state (with a useState) and show an ErrorCard component accordingly or show no error at all. This is because given the way we will be consuming the react-query hooks, we cannot toast an error as our toast function performs a side effect. We would have said this is a limitation if only there is any use case where we might need to perform a side effect when an error occurs where the hooks is being conusmed but there is none as of yet in this app.

# This also means that for all such places as mentioned above, when consuming data from react-query, we will only set the error and display it if and only if there is an error but there is no data. If there is data and yet an error, that means there is stale data and it should still be rendered while we rely on the global toast to alert us of the failed background update. We do a similar flow for loading; it should be set and displayed if and only if loading is true but no data.

# Note: If tomorrow we decide to have a centralized error toast for any react-query error we can just take out the error card from the components where they are being used and just modify the global config (in main.ts) to toast every error not just those for which there is stale data. OR we could also change the algorithm in the global config to ignore background update error and toast the rest.
# --

Form Management

# As much as you can, make sure formId is unique - you can achieve this by using the entire name of of the form, like what the form is in entirety. This will reduce the odds of duplicate formIds significantly.

# FormBuilder: Sole purpose is to provide a default layering for inputs (arranging them in groups) with a submission bar.
# InputLayout: Sole purpose is to help all inputs have a gneral feel in a form UI; the feel include the content of the labelProps only.
# StackedInputsLayout: Serves the exact purpose as the InputLayout, only that this one applies to situations where the Inputs are stacked.
# Whatever situations where an input returns a component already wrapped in InputLayout, then pass a prop called ignoreLayout; an optional boolean prop. Always check this prop before rendering an input component in both FormGroup and StackedInputsLayout to avoid wrapping an Input component in InputLayout where the input component already returns an InputLayout.

# When defining any input (be it standard input or HOC input), always ensure the final output is never returned in an InputLayout, this way the only places where the ignoreLayout prop will be used is in FormGroup and StackedInputsLayout.

# All inputs (including input-layout mappers and hoc inputs) that wishes to benefit from the InputLayout must be wrapped in it when used independently to build forms, however when used in form builder, no need to borther about wrapping them, form builder already handles that.

# Note: There are some components that based on their use case, the user rather defines a custom onChange while consuming them (e.g., ListLookupInput). While for other components, if they are wrapped with InputLayouts, the onChange is automatically taken care of. ALl the consumer needs to do is, in the onSubmit, get the form data and perform any needed transformation on the data before sending to the server. Note, also, that when editing a previously submitted form data, the retrieved data must first be transformed which each value turned into the structure required by it respective input component before sending the data to the form kit as defaultValue.

# As it turns out, the onChange signature of the some input component is a bit different cos ArrayInput was complaining. So, Some input's onChange signature is determined by where they are likely to be used. In any case React Hook Form handles both the `target` and the direct value `onChange` signatures effectively.

# The formKit apparently exposes two methods for setting the value of a field; use the formKit.setValue() for updating field value without enforcing validation. Use formKit.setFieldValue() for updating field value with validation. For the most part, you are likely to use the latter.

# A Critical Observation With React Hook Form (RHK) based on our Form System:
# - When inputs or input groups are conditionally rendered based on another field's value, RHK can mix up field values if the conditional fields are not explicitly set to `null` when the controlling field changes.
# - For example, If "License Info" section (with licensingOrg field) is conditionally shown based on 'category', and 'category' changes, we have observed that RKH may incorrectly assign licensingOrg's value to other fields like 'certifications' which is a totally different field within the form.
# - In some cases of conditional rendering, every field handles it value accurately without the `null` set when the controlling field's value changes and everything works exactly as expected. However, in other cases, you must set all the fields that are rendered conditionally to `null` as the controlling field's value changes else you risk field values mixing up across each other (this exact situation was experienced when building the formInputGroups for editing Freelance Practitioner profile). But in more extreme cases, not just the inputs or input groups that are directly controlled but also all the fields that are controlled by the controlled fields - all must be set to `null` as the main controlling field value changes.

DateInput Component

# Given the generalist nature of this App (particularly, how the FormBuilder is designed with integrated schema-based validation to ensure reliable validation), the DateInput component is built to support all general cases in terms of both validation and accuracy. Similarly, the `TimeInput.

# The DatePickerInput is built in as a complimentary use also. Note that, whenever you use the `DatePickerInput` in forms where schema valiation is very important, make sure enableTyping is false (as is, by default). This is to reserve just the picker to filling in value. Similarly, the `TimePickerInput.

Date, DateTime, and Time - as Server Payloads

# We are currently using Dayjs as our default dateAdapter.
# - Axios, by default, serializes the payload body as JSON using JSON.stringify
# - JSON.stringify calls the toJSON() method of any object it serializes
# - Day.js objects have a toJSON() method, which returns an ISO 8601 string in UTC
# - So effectively, by default a dayjs object always sends a UTC ISO 8601 string over the network and this is exactly what our server always expect for date or dateTime (instants or moments) props.

# When interested in only date:
# - prop type is Date
# - prop name can be date or suffixed with Date

# When interested in date and time as complimentary flow (like an exact instant or moment in time e.g., say a meeting date and time):
# - prop type is Date
# - prop name can be dateTime or suffixed with DateTime

# When interested in only time (HH:mm or HH:mm:ss) as string:
# - prop type is string
# - Server expects just a 'HH:mm' or 'HH:mm:ss' formatted date (dateAdapter()) string

# In other instances the normal Date (exactly as discussed above) can be used for Time. But in such cases, consumer must always note to ignore the date part of the object and focus on the time part when performing any calculations or displaying the value.

Support for Duotones

# For Reference - a case study is in `GroupedMegaMenu.tsx`:
# First, all duotones icons (svgs) were designed with a className called 'secondary'. This className was targeted and given the partial shade in our GlobalStyles.tsx file. Whereever the icon is used, it should be set to inherit; this way the `primary` className will inherit the needed thicker shade color by default. With these setup, if a hover effect is required that respects the duo colors is required, you can target it like so:
#  ':hover': {
#     color: palette.primary.main,
#     svg: {
#       '.secondary': {
#         fill: palette.primary.main,
#         fillOpacity: 0.4,
#       },
#     },
#   },

Money

# All currencies with minor units are fixed at max of 2 decimal places on our system. Currencies that have no decimal places are also handled acccordingly. We picked a general default of 2 d.p. for all currencies with minor unit because this works for most major currencies in the world and also allows for the most precision range possible with JS Number type.

PebbleUI Key Notes

# DESIGN SUMMARY
# Instead of exporting everything from root, we exposed subpaths exports in the package.json (Multi-entry build)
# We support tree-shaking
# The framework is ESM-only (No CJS support cos this is not needed at all).

# FRAMEWORK EXPORT RULES:
# Every "self-sustaining" folder must present it own index.ts file. This will enlists (or exports) the public resources of such a folder. A folder is self containing if it is the most granular unit importable without the possibility of bi-directional or circular dependency issue). The files or components within a folder importing each other should be done without using the index.ts entry of such a folder - they should import directly, while making sure no possibility of bi-directional dependency.

# Any individual file that is self-sustaining (e.g., Single component files that do not fit into any suitable "self-sustaining" folder) basically exports itself either as a default or named export.

# The framework rely strictly on @PublicEntry as the single source of truth for PebbleUI exports. PebbleUI exports are files whose content can be imported by consuming Apps of the framework. Any individual file or barrel export (e.g., index.ts) file that must be exported from the framework must have the marker /** @PublicEntry */ at the topmost layer of the file.

# You can use the typescript /** @internal */ marker to hide individual functions or interfaces from consuming Apps even if the file containing them is marked /** @PublicEntry */.