Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
104f34f
chore: change data-name to className
DasProffi Sep 27, 2026
5238067
feat: update avatar component
DasProffi Sep 27, 2026
9582de5
Merge remote-tracking branch 'origin/main' into feat/update-conventio…
DasProffi Sep 27, 2026
8a5a8b0
chore: remove global export in hightide-web
DasProffi Sep 28, 2026
7b50243
feat: split card and list item logic
DasProffi Sep 28, 2026
e23f0d6
chore: update button and introduce pressable
DasProffi Sep 28, 2026
f846b80
feat: add icon component
DasProffi Sep 28, 2026
464211e
feat: update coloring definitions and add pressable component
DasProffi Sep 29, 2026
8713fcd
chore: remove color transition from interactive elements
DasProffi Sep 29, 2026
b98b366
feat: update VerticalNavigation item
DasProffi Sep 29, 2026
ea7c1a2
feat: split expandable component
DasProffi Sep 29, 2026
248d390
feat: add wheel picker
DasProffi Sep 29, 2026
535c7d1
feat: update date picker and time selection to use wheel pickers
DasProffi Sep 29, 2026
eb292fd
feat: update coloring to differentiate interaction and normal
DasProffi Sep 30, 2026
abbdb48
feat: update description and coloring
DasProffi Sep 30, 2026
c1e4f2d
feat: update select searchbar appearance configuration
DasProffi Sep 30, 2026
7c4da57
feat: update Carsousel component
DasProffi Sep 30, 2026
99c3588
feat: update modal component
DasProffi Sep 30, 2026
38e20fa
chore: update select and input styling
DasProffi Sep 30, 2026
d0d410b
feat: update layout of date time pickers
DasProffi Sep 30, 2026
8d528bb
feat: introduce InputInterface
DasProffi Sep 30, 2026
13a6a81
chore: restructure folders
DasProffi Sep 30, 2026
8d8189d
faet: update coloring attributes
DasProffi Oct 1, 2026
089be94
feat: update input and modal styling
DasProffi Oct 2, 2026
70d1369
fix: table row feedback
DasProffi Oct 2, 2026
9e4f25b
chore: update input interface callbacks
DasProffi Oct 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
7 changes: 7 additions & 0 deletions conventions/Booleans.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Booleans

Every boolean is named `is<Something>` or `has<Something>`.

`is` names a condition the value is in, such as `isGrouped` or `isImageShown`. `has` names something the value possesses, such as `hasStatusIndicator` or `hasImageError`.

A variable that is not a boolean does not start with `is` or `has`.
132 changes: 132 additions & 0 deletions conventions/Components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Components

How components are structured.

## Anatomy

Every component is split into parts, and every component has an aggregate.

The aggregate is the component used in the common case. Parts are the individual elements of that component, attached as static members of the aggregate.

```tsx
<Select value={value} onValueChange={setValue}>
<Select.Option value="a">A</Select.Option>
</Select>
```

`<Select />` is the aggregate. `Select.Root`, `Select.Trigger`, `Select.Content`, and `Select.Option` are its parts.

The aggregate composes those parts into the default arrangement. Callers who need a different arrangement use the parts directly. Both forms are part of the public API.

### Aggregate

Props that belong to an individual part can be set on the aggregate. A dedicated props object for that part overwrites the values the aggregate already applied.

```tsx
<Select
value={value}
onValueChange={setValue}
placeholder="Choose"
triggerProps={{ className: 'w-full' }}
contentProps={{ className: 'max-h-64' }}
>
<Select.Option value="a">A</Select.Option>
</Select>
```

`value` and `onValueChange` parameterize `Select.Root`. `placeholder` parameterizes `Select.Trigger`. `triggerProps` and `contentProps` are applied on top of the props the aggregate already passed, so they win when both set the same prop.

That aggregate is the same composition as:

```tsx
<Select.Root value={value} onValueChange={setValue}>
<Select.Trigger placeholder="Choose" className="w-full" />
<Select.Content className="max-h-64">
<Select.Option value="a">A</Select.Option>
</Select.Content>
</Select.Root>
```

### Parts

Every part of the aggregate is one of these.

#### Context, Provider, and Consumer

When a component has a context, the aggregate always exposes all three.

- `Component.Context` is the React context.
- `Component.Provider` is `Component.Context.Provider`.
- `Component.Consumer` is `Component.Context.Consumer`.

```tsx
<Expandable.Root>
<Expandable.Consumer>
{(state) => (state?.isExpanded ? 'Open' : 'Closed')}
</Expandable.Consumer>
</Expandable.Root>
```

Callers read shared state through `Component.Consumer` or the component hook. They do not reach the consumer by importing the context module.

#### Root

`Component.Root` is the logic of the component. It parameterizes shared state and provides it through context. It renders no HTML element.

The other parts read that context. They do not own the shared state themselves.

`Select.Root` holds the open state, the current value, interaction flags such as disabled, invalid, and read-only, and option registration, and passes them through `Select.Context`.

A component whose parts do not share state does not have a Root.

#### Subcomponent

A subcomponent wraps HTML for one purpose: the trigger, the content panel, an option, and so on. Name it `Component.Element`, and export it on the aggregate.

It contains one HTML element. It is a `forwardRef` to that element, and its props extend `HTMLAttributes` for that element.

When a subcomponent contains more than one HTML element, it still forwards a ref, and every HTML element inside has its own `HTMLAttributes` props.

## Distinction

A component folder matches what the component does.

Grouping folders stay lowercase: `layout`, `interaction`, `data-input`, `visualization`, `properties`, `chat`, and `branding`.

A folder that holds one aggregate uses that aggregate's name, capitalized the same way: `Select`, `Modal`, `Drawer`, `PopUp`, `Table`, `Carousel`, `WheelPicker`, `Avatar`.

### layout

A layout component takes configuration and elements, and arranges those elements. It does not read or write a value through `InputInterface`.

`Modal`, `Carousel`, `Card`, and `Form` are layout components.

### interaction

An interaction component reacts to the user. It does not implement `InputInterface`.

`Button`, `Menu`, `Tooltip`, and `WheelPicker` are interaction components.

### data-input

A data-input component reads and writes a value through `InputInterface`. Every component in `data-input` implements that interface.

`Input`, `Select`, `Combobox`, `Checkbox`, and `DateTimeInput` are data-input components.

### visualization

A visualization component takes required data and displays it. Icons are visualization components.

`Icon`, `Avatar`, `Chip`, and `ProgressIndicator` are visualization components.

### Feature folders

A feature that extends the core components has its own folder. `chat` holds chat features. `branding` holds icons and marks for a specific product.

Inside a feature folder, components use the same folders: `layout`, `interaction`, `data-input`, and `visualization`.

## Styling identification

Components identify themselves for styling with class names. Do not use `data-name`, or any other data attribute, as the styling hook.

Data attributes are reserved for states and configuration, for example `data-disabled`, `data-invalid`, and `data-processing="subtle"`.
99 changes: 99 additions & 0 deletions conventions/Interaction and Feedback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Interaction and Feedback

Minimum states an element must differentiate. These apply to mobile and web unless a state is marked web only.

There are three component types: Presentation, Interactive, and DataInput. Each type lists its own states. States are not inherited from another type.

LoadingState is separate from these types. It describes whether the element is available, waiting on data, blocked by an external operation or dependency, or performing an operation.

## Presentation

Presentation elements display content. They do not take input.

Presentation elements have no interaction states.

## Interactive

Interactive elements can be activated.

| State | Scope |
| --- | --- |
| `hover` | web only |
| `pressed` | mobile and web |
| `focus` | mobile and web |
| `focus-visible` | web only, often replacing `focus` |
| `disabled` | mobile and web |

## DataInput

Data input elements accept or display an editable value.

| State | Scope |
| --- | --- |
| `readonly` | data input only |
| `invalid` | data input only |
| `hover` | web only |
| `pressed` | mobile and web |
| `focus` | mobile and web |
| `focus-visible` | web only, often replacing `focus` |
| `disabled` | mobile and web |

## LoadingState

Every element can be in one loading state:

```ts
type LoadingState = 'idle' | 'loading' | 'blocked' | 'processing'
```

### idle

The element is available to be used.

### loading

The element is loading data. Its content is not ready to show yet.

Show a skeleton or a placeholder in its place. The placeholder is often a rounded rectangle that roughly approximates the element's size.

### blocked

An external operation or dependency is preventing this element from being used.

### processing

This element is updating, or performing an operation, and that operation stops interaction. The element stays visible while the operation runs.

## Interaction states

`readonly`, `invalid`, `hover`, `pressed`, `focus`, `focus-visible`, and `disabled` follow the matching HTML semantics.

### hover

Web only. The pointer is over the element. This matches the CSS `:hover` pseudo-class. Mobile does not use this state.

### pressed

The element is being activated by a pointer or touch press. This matches `:active`.

### focus

The element has focus. This matches `:focus`.

### focus-visible

Web only. The element has focus and that focus should be shown, typically because it came from the keyboard. This matches `:focus-visible`.

On web, `focus-visible` often replaces the `focus` visual, so pointer focus does not draw a focus ring. Mobile does not use this state.

### disabled

The element cannot be interacted with. This matches the `disabled` attribute and `:disabled`.

### readonly

Data input only. The value can be read and is not editable. This matches the `readonly` attribute.

### invalid

Data input only. The current value fails validation. This matches `:invalid` and `aria-invalid`.
48 changes: 48 additions & 0 deletions packages/hightide-utils/src/utils/componentState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { EnumUtilsType } from './enum'

const loadingStateValues = ['idle', 'loading', 'blocked', 'processing'] as const
export type LoadingState = (typeof loadingStateValues)[number]
const allowedLoadingStateValues: ReadonlySet<string> = new Set(loadingStateValues)
function isLoadingStateValue(value: unknown): value is LoadingState {
if (typeof value !== 'string') return false
return allowedLoadingStateValues.has(value)
}
export const LoadingStateUtils: EnumUtilsType<LoadingState> = {
values: loadingStateValues,
set: allowedLoadingStateValues,
isValue: isLoadingStateValue,
}

const interactiveStateValues = ['hover', 'pressed', 'focus', 'focus-visible', 'disabled'] as const
export type InteractiveState = (typeof interactiveStateValues)[number]
const allowedInteractiveStateValues: ReadonlySet<string> = new Set(interactiveStateValues)
function isInteractiveStateValue(value: unknown): value is InteractiveState {
if (typeof value !== 'string') return false
return allowedInteractiveStateValues.has(value)
}
export const InteractiveStateUtils: EnumUtilsType<InteractiveState> = {
values: interactiveStateValues,
set: allowedInteractiveStateValues,
isValue: isInteractiveStateValue,
}

const dataInputStateValues = [
'readonly',
'invalid',
'hover',
'pressed',
'focus',
'focus-visible',
'disabled',
] as const
export type DataInputState = (typeof dataInputStateValues)[number]
const allowedDataInputStateValues: ReadonlySet<string> = new Set(dataInputStateValues)
function isDataInputStateValue(value: unknown): value is DataInputState {
if (typeof value !== 'string') return false
return allowedDataInputStateValues.has(value)
}
export const DataInputStateUtils: EnumUtilsType<DataInputState> = {
values: dataInputStateValues,
set: allowedDataInputStateValues,
isValue: isDataInputStateValue,
}
1 change: 1 addition & 0 deletions packages/hightide-utils/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './array'
export * from './bagFunctions'
export * from './builder'
export * from './componentState'
export * from './curve'
export * from './date'
export * from './duration'
Expand Down
Loading
Loading