Repository: atomicdata-dev/atomic-server (language: rust)

Issue:
Standard Properties - Promote the use of shared properties so we can make richer UI for custom types.
We expect users to use the name and description properties in their own classes so we can show them in the UI. But except for the docs we don't actually tell them they should. How do we promote the use of these properties.

## Option 1

Straight up tell them somewhere in the app.
Not very nice UX but easy

## Option 2 

Add them to classes by default. When making a new class in the ontology editor, add them to the list of recommended or required by default. (This makes more sense for name but not really for description.)
It also doesn't really tell the user why they should actually do this.

## Option 3 

Label them as something like 'Standard Properties' in the resource selector UI.
When the user has not typed anything in the searchbox we show a list of resources of that type. We could sort these standard properties to the top and maybe even give them their own category separated with a divider.


Discussion / hints:
Something like option 3 seems best IMO. 
Don't show AtomicData.dev properties, except for a few recommended hardcoded ones (name, description)

Fix patch:
diff --git a/browser/CHANGELOG.md b/browser/CHANGELOG.md
index faff71ffc..8c8401539 100644
--- a/browser/CHANGELOG.md
+++ b/browser/CHANGELOG.md
@@ -11,12 +11,19 @@ This changelog covers all five packages, as they are (for now) updated as a whol
 - [#992](https://github.com/atomicdata-dev/atomic-server/issues/992) Fix Searchbox overflowing when displaying long names.
 - [#999](https://github.com/atomicdata-dev/atomic-server/issues/999) Fix parseMetaTags character escape issue.
 - [#1014](https://github.com/atomicdata-dev/atomic-server/issues/1014) Fix date input always showing required error even when filled in.
+- [#1008](https://github.com/atomicdata-dev/atomic-server/issues/1005) Showcase standard properties in the resource selector
+- [#1008](https://github.com/atomicdata-dev/atomic-server/issues/1008) Add 'New Property' button to the property list in the ontology editor.
+- [#1008](https://github.com/atomicdata-dev/atomic-server/issues/1008) Fix disabled resource selectors still get highlighted on hover.
+- [#1008](https://github.com/atomicdata-dev/atomic-server/issues/1008) Add 'open' option to classes and properties in the ontology edit view.
+- [#1008](https://github.com/atomicdata-dev/atomic-server/issues/1008) Updated the look of the resource selector and made it more responsive.
+- [#1008](https://github.com/atomicdata-dev/atomic-server/issues/1008) Add info dropdowns to different sections of the ontology editor for more information about the section.
 
 ### @tomic/lib
 
 - `resource.props` is now writeable: `resource.props.name = 'New Name'`.
 - Added `store.preloadResourceTree()` method, see docs for more info.
 - Fix generated ontologies not working in a Next.js server context.
+- SEMI BREAKING CHANGE: When using generated types by cli, @tomic/lib now requires them to be generated by @tomic/cli v0.41.0 or above.
 
 ### @tomic/cli
 
diff --git a/browser/cli/readme.md b/browser/cli/readme.md
index 51e01135a..ca8f9abe2 100644
--- a/browser/cli/readme.md
+++ b/browser/cli/readme.md
@@ -1,15 +1,21 @@
 _Check out [the docs here](https://docs.atomicdata.dev/js-cli)._
 
-`@tomic/cli` is an NPM tool that helps the developer with creating a front-end for their atomic data project by providing typesafety on resources.
+`@tomic/cli` is a command line tool that helps the developer with creating a front-end for their atomic data project by providing typesafety on resources.
 In atomic data you can create [ontologies](https://atomicdata.dev/class/ontology) that describe your business model.
 You can use `@tomic/cli` to generate Typscript types for these ontologies in your front-end.
 
 ```typescript
-import { Post } from './ontolgies/blog'; // <--- generated
+import { type Post } from './ontolgies/blog'; // <--- generated
 
-const myBlogpost = await store.getResourceAsync<Post>(
+const myBlogpost = await store.getResource<Post>(
   'https://myblog.com/atomic-is-awesome',
 );
 
 const comments = myBlogpost.props.comments; // string[] automatically inferred!
+
+myBlogpost.props.name = 'New title';
+myBlogpost.save();
 ```
+
+_Check out [the docs here](https://docs.atomicdata.dev/js-cli)._
+
diff --git a/browser/create-template/templates/sveltekit-site/src/lib/views/MenuItem/MenuItem.svelte b/browser/create-template/templates/sveltekit-site/src/lib/views/MenuItem/MenuItem.svelte
index 1158cb9e1..5b454c6bf 100644
--- a/browser/create-template/templates/sveltekit-site/src/lib/views/MenuItem/MenuItem.svelte
+++ b/browser/create-template/templates/sveltekit-site/src/lib/views/MenuItem/MenuItem.svelte
@@ -49,7 +49,7 @@
 		if (!button || !popover) return;
 
 		// Check if the anchor position api is supported. If so we don't need to calculate the position.
-		if (getComputedStyle(popover).getPropertyValue('--supports-position-anchor') !== 'false') {
+		if (CSS.supports('anchor-name', '--something')) {
 			return;
 		}
 
@@ -142,7 +142,6 @@
 		position-area: bottom center;
 
 		@supports not (anchor-name: --something) {
-			--supports-position-anchor: false;
 			position: fixed;
 			top: var(--top);
 			left: var(--left);
diff --git a/browser/data-browser/src/chunks/GraphViewer/OntologyGraph.tsx b/browser/data-browser/src/chunks/GraphViewer/OntologyGraph.tsx
index 270b6a14d..37183124e 100644
--- a/browser/data-browser/src/chunks/GraphViewer/OntologyGraph.tsx
+++ b/browser/data-browser/src/chunks/GraphViewer/OntologyGraph.tsx
@@ -12,7 +12,7 @@ import { buildGraph } from './buildGraph';
 import { FloatingEdge } from './FloatingEdge';
 import { useGraph } from './useGraph';
 import { useEffectOnce } from '../../hooks/useEffectOnce';
-import { toAnchorId } from '../../views/OntologyPage/toAnchorId';
+import { toAnchorId } from '../../helpers/toAnchorId';
 
 const edgeTypes = {
   floating: FloatingEdge,
diff --git a/browser/data-browser/src/chunks/GraphViewer/buildGraph.ts b/browser/data-browser/src/chunks/GraphViewer/buildGraph.ts
index 69dbd6596..919fe44d5 100644
--- a/browser/data-browser/src/chunks/GraphViewer/buildGraph.ts
+++ b/browser/data-browser/src/chunks/GraphViewer/buildGraph.ts
@@ -183,6 +183,7 @@ export function applyNodeStyling(
       borderColor: theme.colors.bg2,
       color: theme.colors.text,
       borderStyle: node.data.external ? 'dashed' : 'solid',
+      opacity: node.data.external ? 0.7 : 1,
     },
   }));
 }
diff --git a/browser/data-browser/src/components/ClassSelectorDialog.tsx b/browser/data-browser/src/components/ClassSelectorDialog.tsx
index 80ad813c8..7509d6b6c 100644
--- a/browser/data-browser/src/components/ClassSelectorDialog.tsx
+++ b/browser/data-browser/src/components/ClassSelectorDialog.tsx
@@ -89,6 +89,8 @@ const OntologySection = ({ subject, onClassSelect }: OntologySectionProps) => {
         {resource.props.classes?.map(s => (
           <ClassButton key={s} subject={s} onClassSelect={onClassSelect} />
         ))}
+        {!resource.props.classes ||
+          (resource.props.classes.length === 0 && <span>No classes</span>)}
       </Row>
     </OutlinedSection>
   );
diff --git a/browser/data-browser/src/components/Dialog/index.tsx b/browser/data-browser/src/components/Dialog/index.tsx
index 9e70549b3..ef5bdb725 100644
--- a/browser/data-browser/src/components/Dialog/index.tsx
+++ b/browser/data-browser/src/components/Dialog/index.tsx
@@ -17,6 +17,7 @@ import { useDialog } from './useDialog';
 import { useControlLock } from '../../hooks/useControlLock';
 import { useDialogGlobalContext } from './DialogGlobalContextProvider';
 import { DIALOG_CONTENT_CONTAINER } from '../../helpers/containers';
+import { CurrentBackgroundColor } from '../../globalCssVars';
 
 export interface InternalDialogProps {
   show: boolean;
@@ -268,6 +269,7 @@ const fadeInBackground = keyframes`
 `;
 
 const StyledDialog = styled.dialog<{ $width?: CSS.Property.Width }>`
+  ${CurrentBackgroundColor.define(p => p.theme.colors.bg)}
   --dialog-width: min(90vw, ${p => p.$width ?? '60ch'});
 
   ${VAR_DIALOG_INNER_WIDTH}: calc(
@@ -280,7 +282,7 @@ const StyledDialog = styled.dialog<{ $width?: CSS.Property.Width }>`
   z-index: ${p => p.theme.zIndex.dialog};
   padding: ${p => p.theme.size()};
   color: ${props => props.theme.colors.text};
-  background-color: ${props => props.theme.colors.bg};
+  background-color: ${CurrentBackgroundColor.var()};
   border-radius: ${props => props.theme.radius};
   border: solid 1px ${props => props.theme.colors.bg2};
   inline-size: var(--dialog-width);
diff --git a/browser/data-browser/src/components/Dropdown/index.tsx b/browser/data-browser/src/components/Dropdown/index.tsx
index d7e81ec1a..005b19c24 100644
--- a/browser/data-browser/src/components/Dropdown/index.tsx
+++ b/browser/data-browser/src/components/Dropdown/index.tsx
@@ -378,6 +378,7 @@ export function MenuItem({
 
 const StyledShortcut = styled(Shortcut)`
   margin-left: 0.3rem;
+  color: ${p => p.theme.colors.textLight};
 `;
 
 const StyledLabel = styled.span`
@@ -389,44 +390,46 @@ interface MenuItemStyledProps {
 }
 
 const MenuItemStyled = styled(Button)<MenuItemStyledProps>`
+  --menu-item-bg: ${p =>
+    p.selected ? p.theme.colors.mainSelectedBg : p.theme.colors.bg};
+  --menu-item-fg: ${p =>
+    p.selected ? p.theme.colors.mainSelectedFg : p.theme.colors.text};
   align-items: center;
   display: flex;
   gap: 0.5rem;
   width: 100%;
   text-align: left;
-  color: ${p => p.theme.colors.text};
+  color: var(--menu-item-fg);
   padding: 0.4rem 1rem;
   height: auto;
-  background-color: ${p =>
-    p.selected ? p.theme.colors.bg1 : p.theme.colors.bg};
-  text-decoration: ${p => (p.selected ? 'underline' : 'none')};
+  background-color: var(--menu-item-bg);
+  outline: none;
 
   & svg {
-    color: ${p => p.theme.colors.textLight};
+    color: var(--menu-item-fg);
   }
+
   &:hover {
-    background-color: ${p => p.theme.colors.bg1};
+    --menu-item-bg: ${p => p.theme.colors.mainSelectedBg};
+    --menu-item-fg: ${p => p.theme.colors.mainSelectedFg};
+
+    @media (prefers-contrast: more) {
+      --menu-item-bg: ${p => (p.theme.darkMode ? 'white' : 'black')};
+      --menu-item-fg: ${p => (p.theme.darkMode ? 'black' : 'white')};
+    }
   }
   &:active {
-    background-color: ${p => p.theme.colors.bg2};
+    filter: brightness(0.9);
   }
   &:disabled {
     color: ${p => p.theme.colors.textLight2};
     cursor: default;
     background-color: ${p => p.theme.colors.bg};
 
-    &:hover {
-      cursor: 'default';
-    }
-
     & svg {
       color: ${p => p.theme.colors.textLight2};
     }
   }
-
-  svg {
-    color: ${p => p.theme.colors.textLight};
-  }
 `;
 
 const ItemDivider = styled.div`
@@ -450,9 +453,13 @@ const Menu = styled.div<MenuProps>`
   width: auto;
   box-shadow: ${p => p.theme.boxShadowSoft};
   opacity: ${p => (p.isActive ? 1 : 0)};
+  ${transition('opacity')};
 
   @starting-style {
     opacity: 0;
   }
-  ${transition('opacity')};
+
+  @media (prefers-contrast: more) {
+    border: solid 1px ${p => p.theme.colors.bg2};
+  }
 `;
diff --git a/browser/data-browser/src/components/OutlinedSection.tsx b/browser/data-browser/src/components/OutlinedSection.tsx
index cd2adc8c8..2ab5b4a01 100644
--- a/browser/data-browser/src/components/OutlinedSection.tsx
+++ b/browser/data-browser/src/components/OutlinedSection.tsx
@@ -1,6 +1,7 @@
 import { PropsWithChildren } from 'react';
 import { Row } from './Row';
 import { styled } from 'styled-components';
+import { CurrentBackgroundColor } from '../globalCssVars';
 
 interface OutlinedSectionProps {
   title: string;
@@ -39,7 +40,7 @@ const Heading = styled.h2`
   color: ${p => p.theme.colors.textLight};
   font-weight: normal;
   margin: 0;
-  background-color: ${p => p.theme.colors.bgBody};
+  background-color: ${CurrentBackgroundColor.var()};
   position: absolute;
   top: -0.5rem;
   left: ${p => p.theme.size()};
diff --git a/browser/data-browser/src/components/ResourceContextMenu/index.tsx b/browser/data-browser/src/components/ResourceContextMenu/index.tsx
index 2900db053..720f85626 100644
--- a/browser/data-browser/src/components/ResourceContextMenu/index.tsx
+++ b/browser/data-browser/src/components/ResourceContextMenu/index.tsx
@@ -25,6 +25,7 @@ import {
   FaShare,
   FaTrash,
   FaPlus,
+  FaArrowUpRightFromSquare,
 } from 'react-icons/fa6';
 import { useQueryScopeHandler } from '../../hooks/useQueryScope';
 import {
@@ -50,6 +51,7 @@ export enum ContextMenuOptions {
   UseInCode = 'useInCode',
   NewChild = 'newChild',
   Export = 'export',
+  Open = 'open',
 }
 
 export interface ResourceContextMenuProps {
@@ -64,6 +66,7 @@ export interface ResourceContextMenuProps {
   /** Callback that is called after the resource was deleted */
   onAfterDelete?: () => void;
   title?: string;
+  external?: boolean;
 }
 
 /** Dropdown menu that opens a bunch of actions for some resource */
@@ -74,6 +77,7 @@ function ResourceContextMenu({
   simple,
   isMainMenu,
   title,
+  external,
   bindActive,
   onAfterDelete,
 }: ResourceContextMenuProps) {
@@ -132,10 +136,16 @@ function ResourceContextMenu({
       },
       DIVIDER,
     ),
+    ...addIf(!!external, {
+      id: ContextMenuOptions.Open,
+      label: 'Open',
+      helper: 'Open the resource',
+      onClick: () => navigate(constructOpenURL(subject)),
+      icon: <FaArrowUpRightFromSquare />,
+    }),
     ...addIf(
       canWrite,
       {
-        // disabled: !canWrite || location.pathname.startsWith(paths.edit),
         id: ContextMenuOptions.Edit,
         label: 'Edit',
         helper: 'Open the edit form.',
diff --git a/browser/data-browser/src/components/forms/SearchBox/ResultLine.tsx b/browser/data-browser/src/components/forms/SearchBox/ResultLine.tsx
index 52a5f82f0..794845030 100644
--- a/browser/data-browser/src/components/forms/SearchBox/ResultLine.tsx
+++ b/browser/data-browser/src/components/forms/SearchBox/ResultLine.tsx
@@ -1,13 +1,15 @@
 import {
+  core,
   dataBrowser,
-  urls,
   useResource,
   useString,
   type Resource,
 } from '@tomic/react';
 import React, { useEffect, useRef } from 'react';
 import { styled, css } from 'styled-components';
-import { getIconForClass } from '../../../helpers/iconMap';
+import { dataTypeIconMap, getIconForClass } from '../../../helpers/iconMap';
+import { FaAtom } from 'react-icons/fa6';
+import { Row } from '../../Row';
 
 interface ResultLineProps {
   selected: boolean;
@@ -26,19 +28,25 @@ export function ResultLine({
   onClick,
 }: React.PropsWithChildren<ResultLineProps>): JSX.Element {
   const ref = useRef<HTMLLIElement>(null);
+  // We need to track mouse hover state but we don't want to re-render the component on every mouse move.
+  const hasMouseHover = useRef(false);
 
   useEffect(() => {
-    if (selected) {
+    if (selected && !hasMouseHover.current) {
       ref.current?.scrollIntoView({ block: 'nearest' });
     }
-  }, [selected]);
+  }, [selected, hasMouseHover]);
 
   return (
     <ListItem
       selected={selected}
       ref={ref}
       tabIndex={-1}
-      onMouseMove={() => onMouseOver()}
+      onMouseMove={() => {
+        hasMouseHover.current = true;
+        onMouseOver();
+      }}
+      onMouseLeave={() => (hasMouseHover.current = false)}
       onClick={onClick}
     >
       {children}
@@ -51,13 +59,20 @@ export function ResourceResultLine({
   ...props
 }: ResourceResultLineProps): JSX.Element {
   const resource = useResource(subject);
-  const [description] = useString(resource, urls.properties.description);
+  const [description] = useString(resource, core.properties.description);
 
   return (
     <ResultLine {...props}>
-      <Icon resource={resource} />
-      {resource.title}
-      {description && <Description> - {description.slice(0, 70)}</Description>}
+      <Row gap='1ch' center>
+        <Icon resource={resource} />
+        <Name>{resource.title}</Name>
+      </Row>
+      {description && (
+        <Description>
+          {description.slice(0, 70).trim()}
+          {description.length > 70 ? '...' : ''}
+        </Description>
+      )}
     </ResultLine>
   );
 }
@@ -67,12 +82,15 @@ type IconProps = {
 };
 
 function Icon({ resource }: IconProps): React.ReactElement {
-  const IconComp = getIconForClass(resource.getClasses()[0] ?? '');
+  let IconComp = getIconForClass(resource.getClasses()[0] ?? '');
 
   if (resource.hasClasses(dataBrowser.classes.tag)) {
     const emoji = resource.get(dataBrowser.properties.emoji);
 
     return emoji ? <span>{emoji}</span> : <IconComp />;
+  } else if (resource.hasClasses(core.classes.property)) {
+    IconComp =
+      dataTypeIconMap.get(resource.get(core.properties.datatype)) ?? FaAtom;
   }
 
   return <IconComp />;
@@ -81,36 +99,63 @@ function Icon({ resource }: IconProps): React.ReactElement {
 const Description = styled.span`
   white-space: nowrap;
   color: ${({ theme }) => theme.colors.textLight};
+  grid-column: 2/2;
+  overflow: hidden;
+  text-overflow: ellipsis;
 `;
 
-export const ListItem = styled.li<{ selected: boolean }>`
+export const ListItem = styled.li<{ selected: boolean; gridColumn?: string }>`
+  --list-item-bg: none;
+  --list-item-color: currentColor;
+  --list-item-svg-color: ${({ theme }) => theme.colors.textLight};
+
+  background-color: var(--list-item-bg);
+  color: var(--list-item-color);
+  grid-column: 1/3;
   padding: 0.5rem;
   list-style: none;
   margin: 0;
-  padding-left: ${({ theme }) => theme.margin}rem;
-  border-bottom: 1px solid ${({ theme }) => theme.colors.bg2};
-  min-width: 100%;
-  width: 100%;
-  text-overflow: ellipsis;
+  padding-left: ${({ theme }) => theme.size()};
+  width: 100cqw;
   white-space: nowrap;
-  overflow: hidden;
-  display: flex;
-  align-items: center;
-  gap: 0.7ch;
+  display: grid;
+  grid-template-columns: subgrid;
+  grid-template-rows: 1fr;
 
   cursor: pointer;
+  &:has(+ div) {
+    padding-bottom: 1rem;
+  }
 
-  ${p =>
-    p.selected &&
-    css`
-      box-shadow: inset 0 0 0px 1px ${({ theme }) => theme.colors.main};
-      color: ${({ theme }) => theme.colors.main};
-    `}
+  div + & {
+    padding-top: 1rem;
+  }
 
   svg {
-    color: ${({ selected, theme }) =>
-      selected ? theme.colors.main : theme.colors.textLight};
+    color: var(--list-item-svg-color);
     min-width: 1rem;
     height: 1rem;
   }
+
+  ${({ selected, theme }) =>
+    selected &&
+    css`
+      --list-item-bg: ${theme.colors.mainSelectedBg};
+      --list-item-color: ${theme.colors.mainSelectedFg};
+      --list-item-svg-color: var(--list-item-color);
+
+      @media (prefers-contrast: more) {
+        --list-item-bg: ${theme.darkMode ? 'white' : 'black'};
+        --list-item-color: ${theme.darkMode ? 'black' : 'white'};
+      }
+    `}
+
+  @container (max-width: 520px) {
+    grid-column: 1/2;
+  }
+`;
+
+const Name = styled.span`
+  overflow: hidden;
+  text-overflow: ellipsis;
 `;
diff --git a/browser/data-browser/src/components/forms/SearchBox/SearchBox.tsx b/browser/data-browser/src/components/forms/SearchBox/SearchBox.tsx
index 7bc58a09e..cddd33106 100644
--- a/browser/data-browser/src/components/forms/SearchBox/SearchBox.tsx
+++ b/browser/data-browser/src/components/forms/SearchBox/SearchBox.tsx
@@ -279,14 +279,15 @@ const TriggerButtonWrapper = styled.div<{
 
   background-color: ${p => SB_BACKGROUND.var(p.theme.colors.bg)};
 
-  &:disabled {
+  &:has(:disabled) {
     background-color: ${props => props.theme.colors.bg1};
+    opacity: 0.7;
   }
 
   &:has(${TriggerButton}:hover(), ${TriggerButton}:focus-visible) {
   }
-  &:hover,
-  &:focus-visible {
+
+  &:not(:has(:disabled)):where(:hover, :focus-visible) {
     border-color: transparent;
     box-shadow: 0 0 0 2px ${SB_HIGHLIGHT.var()};
     ${p =>
diff --git a/browser/data-browser/src/components/forms/SearchBox/SearchBoxWindow.tsx b/browser/data-browser/src/components/forms/SearchBox/SearchBoxWindow.tsx
index f06c900e2..d12152bc3 100644
--- a/browser/data-browser/src/components/forms/SearchBox/SearchBoxWindow.tsx
+++ b/browser/data-browser/src/components/forms/SearchBox/SearchBoxWindow.tsx
@@ -1,4 +1,4 @@
-import { core, useResources, useServerSearch } from '@tomic/react';
+import { core, dataBrowser, useResources, useServerSearch } from '@tomic/react';
 import {
   ClipboardEventHandler,
   KeyboardEventHandler,
@@ -23,6 +23,31 @@ import { useSettings } from '../../../helpers/AppSettings';
 import { QuickScore } from 'quick-score';
 import { useTitlePropOfClass } from '../ResourceSelector/useTitlePropOfClass';
 import { stringToSlug } from '../../../helpers/stringToSlug';
+import { addIf } from '../../../helpers/addIf';
+import { Row } from '../../Row';
+
+/**
+ * Options shown at the top of the results when the `isA` prop matches a key in this object.
+ */
+const STANDARD_OPTIONS: Record<string, string[]> = {
+  [core.classes.property]: [
+    core.properties.name,
+    core.properties.description,
+    core.properties.shortname,
+    dataBrowser.properties.image,
+  ],
+};
+
+enum OptionType {
+  CreateOption,
+  StandardOption,
+  Result,
+}
+
+type Option = {
+  type: OptionType;
+  data: string;
+};
 
 const BOX_HEIGHT_REM = 20;
 
@@ -39,6 +64,10 @@ interface SearchBoxWindowProps {
   onCreateItem?: (name: string, isA?: string) => void;
 }
 
+/**
+ * The window that opens when the searchbox is focussed.
+ * It handles searching, both locally and on the server.
+ */
 export function SearchBoxWindow({
   searchValue,
   onChange,
@@ -53,7 +82,7 @@ export function SearchBoxWindow({
 }: SearchBoxWindowProps): JSX.Element {
   const { drive } = useSettings();
 
-  const [realIndex, setIndex] = useState<number | undefined>(undefined);
+  const [index, setIndex] = useState<number | undefined>(undefined);
   const [results, setResults] = useState<string[]>([]);
   const [searchError, setSearchError] = useState<Error | undefined>();
   const [valueIsURL, setValueIsURL] = useState(false);
@@ -67,12 +96,31 @@ export function SearchBoxWindow({
   const showCreateOption =
     onCreateItem && searchValue && !valueIsURL && !allowsOnly;
 
-  const offset = showCreateOption ? 1 : 0;
+  const standardOptions = useMemo(() => {
+    if (!searchValue && isA && isA in STANDARD_OPTIONS) {
+      return STANDARD_OPTIONS[isA];
+    }
+
+    return [];
+  }, [isA, searchValue]);
+
+  const options: Option[] = useMemo(
+    () => [
+      ...addIf(!!showCreateOption, {
+        type: OptionType.CreateOption,
+        data: '',
+      }),
+      ...standardOptions.map(option => ({
+        type: OptionType.StandardOption,
+        data: option,
+      })),
+      ...results.map(result => ({ type: OptionType.Result, data: result })),
+    ],
+    [showCreateOption, standardOptions, results],
+  );
 
   const selectedIndex =
-    realIndex !== undefined
-      ? loopingIndex(realIndex, results.length + offset)
-      : undefined;
+    index !== undefined ? loopingIndex(index, options.length) : undefined;
 
   const handleKeyDown: KeyboardEventHandler<HTMLInputElement> = e => {
     if (e.key === 'Enter') {
@@ -139,20 +187,24 @@ export function SearchBoxWindow({
     onCreateItem(name, isA);
   };
 
-  const pickSelectedItem = () => {
-    if (selectedIndex === undefined) {
+  const pickSelectedItem = (override?: number) => {
+    if (selectedIndex === undefined && override === undefined) {
       onSelect(searchValue);
 
       return;
     }
 
-    if (selectedIndex === 0 && showCreateOption) {
+    const selected = options[override! ?? selectedIndex!];
+
+    // The selected option is a "Create ..." option.
+    if (selected.type === OptionType.CreateOption) {
       createItem(searchValue);
 
       return;
     }
 
-    onSelect(results[selectedIndex - offset]);
+    // The selected option is a standard option or a search result.
+    onSelect(selected.data);
   };
 
   const handleResults = useCallback((res: string[], error?: Error) => {
@@ -206,7 +258,12 @@ export function SearchBoxWindow({
   }
 
   return (
-    <Wrapper onBlur={handleBlur} ref={wrapperRef} $above={isAboveTrigger}>
+    <Wrapper
+      onBlur={handleBlur}
+      ref={wrapperRef}
+      $above={isAboveTrigger}
+      onMouseLeave={() => setIndex(undefined)}
+    >
       <SearchInputWrapper>
         <FaSearch />
         <Input
@@ -223,33 +280,55 @@ export function SearchBoxWindow({
           <CenteredMessage>Start Searching</CenteredMessage>
         )}
         <StyledScrollArea>
-          <ul>
-            {showCreateOption ? (
-              <ResultLine
-                selected={selectedIndex === 0}
-                onMouseOver={() => handleMouseMove(0)}
-                onClick={() => createItem(searchValue)}
-              >
-                {titleProp ? (
-                  <>
-                    Create{' '}
-                    <CreateLineInputText>{searchValue}</CreateLineInputText>
-                  </>
-                ) : (
-                  `Create new ${classTitle ?? 'resource'}`
-                )}
-              </ResultLine>
-            ) : null}
-            {results.map((result, i) => (
-              <ResourceResultLine
-                key={result}
-                subject={result}
-                selected={i + offset === selectedIndex}
-                onMouseOver={() => handleMouseMove(i + offset)}
-                onClick={pickSelectedItem}
-              />
-            ))}
-          </ul>
+          <List>
+            {options.map((option, i) => {
+              let line = <></>;
+
+              if (option.type === OptionType.CreateOption) {
+                line = (
+                  <ResultLine
+                    key={'create option'}
+                    selected={selectedIndex === 0}
+                    onMouseOver={() => handleMouseMove(0)}
+                    onClick={() => createItem(searchValue)}
+                  >
+                    {titleProp ? (
+                      <Row gap='0.5ch'>
+                        Create{' '}
+                        <CreateLineInputText>{searchValue}</CreateLineInputText>
+                      </Row>
+                    ) : (
+                      `Create new ${classTitle ?? 'resource'}`
+                    )}
+                  </ResultLine>
+                );
+              } else {
+                line = (
+                  <ResourceResultLine
+                    key={option.data}
+                    subject={option.data}
+                    selected={i === selectedIndex}
+                    onMouseOver={() => handleMouseMove(i)}
+                    onClick={() => {
+                      // On mobile the item is not selected by hover so we need to force pickSelectedItem to pick this one.
+                      pickSelectedItem(i);
+                    }}
+                  />
+                );
+              }
+
+              const showDivider =
+                options[i + 1] !== undefined &&
+                option.type !== options[i + 1].type;
+
+              return (
+                <>
+                  {line}
+                  {showDivider && <Divider />}
+                </>
+              );
+            })}
+          </List>
           {!!searchValue && results.length === 0 && (
             <CenteredMessage>No Results</CenteredMessage>
           )}
@@ -301,7 +380,11 @@ const ServerSearchUnit = ({
       filters: {
         ...(isA ? { [core.properties.isA]: isA } : {}),
       },
-      parents: scopes ?? [drive, 'https://atomicdata.dev'],
+      parents: scopes ?? [
+        drive,
+        // We don't want to show atomicdata.dev results when there are standard defined options.
+        ...addIf(!!isA && !(isA in STANDARD_OPTIONS), 'https://atomicdata.dev'),
+      ],
       // If a classtype is given we want to prefill the searchbox with data.
       allowEmptyQuery: !!isA,
     }),
@@ -384,13 +467,26 @@ const Input = styled.input`
 `;
 
 const ResultBox = styled.div`
+  container: searchbox / inline-size;
   flex: 1;
   border: solid 1px ${p => p.theme.colors.bg2};
-
   height: calc(100% - 2rem);
   overflow: hidden;
 `;
 
+const List = styled.ul`
+  display: grid;
+  grid-template-columns: 20ch auto;
+  column-gap: 1ch;
+  overflow: hidden;
+  width: calc(100cqw);
+  margin-bottom: 0;
+
+  @container (max-width: 520px) {
+    grid-template-columns: 1fr;
+  }
+`;
+
 const Wrapper = styled.div<{ $above: boolean }>`
   display: flex;
 
@@ -401,6 +497,9 @@ const Wrapper = styled.div<{ $above: boolean }>`
   height: ${BOX_HEIGHT_REM}rem;
   position: absolute;
   width: var(--radix-popover-trigger-width);
+  left: 0;
+  animation: ${fadeIn} 0.2s ease-in-out;
+
   ${({ $above, theme }) =>
     $above
       ? css`
@@ -433,9 +532,6 @@ const Wrapper = styled.div<{ $above: boolean }>`
             border-bottom-right-radius: ${p => p.theme.radius};
           }
         `}
-  left: 0;
-
-  animation: ${fadeIn} 0.2s ease-in-out;
 `;
 const CenteredMessage = styled.div`
   display: grid;
@@ -454,3 +550,12 @@ const CreateLineInputText = styled.span`
   color: ${p => p.theme.colors.textLight};
   font-style: italic;
 `;
+
+const Divider = styled.div`
+  border-top: 1px solid ${props => props.theme.colors.bg2};
+  grid-column: 1/3;
+
+  @container (max-width: 520px) {
+    grid-column: 1/2;
+  }
+`;
diff --git a/browser/data-browser/src/globalCssVars.ts b/browser/data-browser/src/globalCssVars.ts
new file mode 100644
index 000000000..fff29c1ed
--- /dev/null
+++ b/browser/data-browser/src/globalCssVars.ts
@@ -0,0 +1,3 @@
+import { CSSVar } from './helpers/CSSVar';
+
+export const CurrentBackgroundColor = new CSSVar('current-background-color');
diff --git a/browser/data-browser/src/views/OntologyPage/toAnchorId.ts b/browser/data-browser/src/helpers/toAnchorId.ts
similarity index 100%
rename from browser/data-browser/src/views/OntologyPage/toAnchorId.ts
rename to browser/data-browser/src/helpers/toAnchorId.ts
diff --git a/browser/data-browser/src/styling.tsx b/browser/data-browser/src/styling.tsx
index 6b1e01ce6..94f9e0f72 100644
--- a/browser/data-browser/src/styling.tsx
+++ b/browser/data-browser/src/styling.tsx
@@ -3,10 +3,11 @@ import {
   DefaultTheme,
   ThemeProvider,
 } from 'styled-components';
-import { darken, lighten } from 'polished';
+import { darken, lighten, setLightness } from 'polished';
 import './reset.css';
 import { useContext } from 'react';
 import { SettingsContext } from './helpers/AppSettings';
+import { CurrentBackgroundColor } from './globalCssVars';
 
 interface ThemeWrapperProps {
   children: React.ReactNode;
@@ -118,6 +119,8 @@ export const buildTheme = (darkMode: boolean, mainIn: string): DefaultTheme => {
       bg: bg,
       // Use pitch black for dark mode
       bgBody: darkMode ? bg : darken(0.02)(bg),
+      mainSelectedBg: setLightness(darkMode ? 0.05 : 0.97, main),
+      mainSelectedFg: setLightness(darkMode ? 0.7 : 0.25, main),
       bg1: darkMode ? lighten(0.1)(bg) : darken(0.05)(bg),
       bg2: darkMode ? lighten(0.3)(bg) : darken(0.2)(bg),
       text,
@@ -198,6 +201,10 @@ declare module 'styled-components' {
       mainLight: string;
       /** Slightly darker version of Main accent color */
       mainDark: string;
+      /** Background color of selected items */
+      mainSelectedBg: string;
+      /** Foreground color of selected items */
+      mainSelectedFg: string;
       /** The background color of the body, which is subtly different from bg */
       bgBody: string;
       /** Most common background color */
@@ -257,7 +264,8 @@ export const GlobalStyle = createGlobalStyle`
   }
 
   body {
-    background-color: ${props => props.theme.colors.bgBody};
+    ${CurrentBackgroundColor.define(p => p.theme.colors.bgBody)}
+    background-color: ${CurrentBackgroundColor.var()};
     color: ${props => props.theme.colors.text};
     font-family: ${props => props.theme.fontFamily};
     line-height: 1.5em;
diff --git a/browser/data-browser/src/views/CodeUsage/generators/BasicCodeGenerator.ts b/browser/data-browser/src/views/CodeUsage/generators/BasicCodeGenerator.ts
index e30f39072..94e2bbd3b 100644
--- a/browser/data-browser/src/views/CodeUsage/generators/BasicCodeGenerator.ts
+++ b/browser/data-browser/src/views/CodeUsage/generators/BasicCodeGenerator.ts
@@ -136,10 +136,10 @@ ${propUsage}
     return [
       `<script>
   ${imports}
-  let resource = getResource('${this.resource.subject}');
+  let resource = getResource(() => '${this.resource.subject}');
 </script>
 
-<div>{$resource.title}</div> `,
+<div>{resource.title}</div> `,
     ];
   }
 
@@ -152,21 +152,17 @@ ${propUsage}
         name: 'getResource',
         file: '@tomic/svelte',
       },
-      {
-        name: 'getValue',
-        file: '@tomic/svelte',
-      },
       propImport,
     );
 
     return [
       `<script>
 ${imports}
-  let resource = getResource('${this.resource.subject}');
-  let value = getValue(resource, ${propSubjectRef});
+  let resource = getResource(() => '${this.resource.subject}');
+  let value = $derived(resource.get(${propSubjectRef}));
 </script>
 
-<div>{$value}</div>`,
+<div>{value}</div>`,
     ];
   }
 
@@ -179,10 +175,10 @@ ${imports}
     return [
       `<script lang='ts'>
   ${imports}
-  let resource = getResource('${this.resource.subject}');
+  let resource = getResource(() => '${this.resource.subject}');
 </script>
 
-<div>{$resource.title}</div>`,
+<div>{resource.title}</div>`,
     ];
   }
 
@@ -200,24 +196,20 @@ ${imports}
         name: 'getResource',
         file: '@tomic/svelte',
       },
-      {
-        name: 'getValue',
-        file: '@tomic/svelte',
-      },
       resourceShorthand ? undefined : propImport,
     );
 
     const hookPart = resourceShorthand
       ? ''
-      : `\n  let value = getValue(resource, ${propSubjectRef});`;
+      : `\n  let value = $derived(resource.get(${propSubjectRef}));`;
 
     return [
       `<script lang='ts'>
 ${imports}
-  let resource = getResource${genericName}('${this.resource.subject}');${hookPart}
+  let resource = getResource${genericName}(() => '${this.resource.subject}');${hookPart}
 </script>
 
-<div>{${resourceShorthand ?? '$value'}}</div> `,
+<div>{${resourceShorthand ?? 'value'}}</div> `,
     ];
   }
 }
diff --git a/browser/data-browser/src/views/CodeUsage/generators/TableCodeGenerator.ts b/browser/data-browser/src/views/CodeUsage/generators/TableCodeGenerator.ts
index d013493e2..58319ac1a 100644
--- a/browser/data-browser/src/views/CodeUsage/generators/TableCodeGenerator.ts
+++ b/browser/data-browser/src/views/CodeUsage/generators/TableCodeGenerator.ts
@@ -99,7 +99,7 @@ for await (const rowSubject of table) {
     const valueLine = resourceShorthand
       ? `  console.log(\`\${row.title}: \${${resourceShorthand}}\`);`
       : `  const value = row.get(${propSubjectRef});
-  console.log(value);`;
+  console.log(\`\${row.title}: \${value}\`);`;
 
     return [
       `${imports}
@@ -113,7 +113,7 @@ const table = new CollectionBuilder(store)
 // Iterate over the collection, fetch the children and log a value
 // Check the docs on how to use collection for other use cases like pagenation
 for await (const rowSubject of table) {
-  const row = await store.getResource${genericName}(rowSubject);
+  const row = await store.getResource${genericName ?? ''}(rowSubject);
 ${valueLine}
 }`,
     ];
@@ -298,7 +298,7 @@ const Component = () => {
 };
 
 const Row = ({ subject }: { subject: string }) => {
-  const row = useResource${genericName}(subject);
+  const row = useResource${genericName ?? ''}(subject);
 ${propUsage}
 };`,
     ];
@@ -310,20 +310,24 @@ ${propUsage}
       `// Component.svelte
 <script>
   import { CollectionBuilder } from '@tomic/lib';
-  import { store } from '@tomic/svelte';
+  import { getStoreFromContext } from '@tomic/svelte';
 
-  let page = 0;
-  let items = [];
+  const store = getStoreFromContext();
+
+  let page = $state(0);
+  let items = $state([]);
 
   // Create a collection containing the children of the table
-  const table = new CollectionBuilder($store)
+  const table = new CollectionBuilder(store)
     .setProperty(core.properties.parent)
     .setValue('${this.resource.subject}')
     .setSortBy(commits.properties.createdAt)
     .build();
 
-  $: table.getMembersOnPage(page).then(members => {
-    items = members;
+  $effect(() => {
+    table.getMembersOnPage(page).then(members => {
+      items = members;
+    });
   });
 </script>
 
@@ -334,19 +338,19 @@ ${propUsage}
     </li>
   {/each}
 </ul>
-<button on:click={() => page -= 1}>Prev</button>
-<button on:click={() => page += 1}>Next</button>`,
+<button onclick={() => page -= 1}>Prev</button>
+<button onclick={() => page += 1}>Next</button>`,
       // Item code
       `// Item.svelte
 <script>
   import { getResource } from '@tomic/svelte';
 
-  export let subject: string;
+  let { subject } = $props();
 
-  let resource = getResource(subject);
+  let resource = getResource(() => subject);
 </script>
 
-<span>{$resource.title}</span>`,
+<span>{resource.title}</span>`,
     ];
   }
 
@@ -359,10 +363,6 @@ ${propUsage}
         name: 'getResource',
         file: '@tomic/svelte',
       },
-      {
-        name: 'getValue',
-        file: '@tomic/svelte',
-      },
       propImport,
     );
 
@@ -371,20 +371,24 @@ ${propUsage}
       `// Component.svelte
 <script>
   import { CollectionBuilder } from '@tomic/lib';
-  import { store } from '@tomic/svelte';
+  import { getStoreFromContext } from '@tomic/svelte';
+
+  const store = getStoreFromContext();
 
-  let page = 0;
-  let items = [];
+  let page = $state(0);
+  let items = $state([]);
 
   // Create a collection containing the children of the table
-  const table = new CollectionBuilder($store)
+  const table = new CollectionBuilder(store)
     .setProperty(core.properties.parent)
     .setValue('${this.resource.subject}')
     .setSortBy(commits.properties.createdAt)
     .build();
 
-  $: table.getMembersOnPage(page).then(members => {
-    items = members;
+  $effect(() => {
+    table.getMembersOnPage(page).then(members => {
+      items = members;
+    });
   });
 </script>
 
@@ -395,19 +399,19 @@ ${propUsage}
     </li>
   {/each}
 </ul>
-<button on:click={() => page -= 1}>Prev</button>
-<button on:click={() => page += 1}>Next</button>`,
+<button onclick={() => page -= 1}>Prev</button>
+<button onclick={() => page += 1}>Next</button>`,
       // Item code
       `// Item.svelte
 <script>
 ${imports}
-  export let subject: string;
+  let { subject } = $props();
 
-  let resource = getResource(subject);
-  let value = getValue(resource, ${propSubjectRef});
+  let resource = getResource(() => subject);
+  let value = $derived(resource.get(${propSubjectRef}));
 </script>
 
-<span>{$resource.title}: {$value}</span>`,
+<span>{resource.title}: {value}</span>`,
     ];
   }
 
@@ -417,20 +421,24 @@ ${imports}
       `// Component.svelte
 <script lang='ts'>
   import { CollectionBuilder } from '@tomic/lib';
-  import { store } from '@tomic/svelte';
+  import { getStoreFromContext } from '@tomic/svelte';
+
+  const store = getStoreFromContext();
 
-  let page = 0;
-  let items = [];
+  let page = $state(0);
+  let items = $state([]);
 
   // Create a collection containing the children of the table
-  const table = new CollectionBuilder($store)
+  const table = new CollectionBuilder(store)
     .setProperty(core.properties.parent)
     .setValue('${this.resource.subject}')
     .setSortBy(commits.properties.createdAt)
     .build();
 
-  $: table.getMembersOnPage(page).then(members => {
-    items = members;
+  $effect(() => {
+    table.getMembersOnPage(page).then(members => {
+      items = members;
+    });
   });
 </script>
 
@@ -441,19 +449,23 @@ ${imports}
     </li>
   {/each}
 </ul>
-<button on:click={() => page -= 1}>Prev</button>
-<button on:click={() => page += 1}>Next</button>`,
+<button onclick={() => page -= 1}>Prev</button>
+<button onclick={() => page += 1}>Next</button>`,
       // Item code
       `// Item.svelte
 <script lang='ts'>
   import { getResource } from '@tomic/svelte';
 
-  export let subject: string;
+  type Props = {
+    subject: string;
+  };
 
-  let resource = getResource(subject);
+  let { subject }: Props = $props();
+
+  let resource = getResource(() => subject);
 </script>
 
-<span>{$resource.title}</span>`,
+<span>{resource.title}</span>`,
     ];
   }
 
@@ -471,36 +483,36 @@ ${imports}
         name: 'getResource',
         file: '@tomic/svelte',
       },
-      {
-        name: 'getValue',
-        file: '@tomic/svelte',
-      },
       resourceShorthand ? undefined : propImport,
     );
 
     const hookPart = resourceShorthand
       ? ''
-      : `\n  let value = getValue(resource, ${propSubjectRef});`;
+      : `\n  let value = $derived(resource.get(${propSubjectRef}));`;
 
     return [
       // Component code
       `// Component.svelte
 <script lang='ts'>
   import { CollectionBuilder } from '@tomic/lib';
-  import { store } from '@tomic/svelte';
+  import { getStoreFromContext } from '@tomic/svelte';
+
+  const store = getStoreFromContext();
 
-  let page = 0;
-  let items = [];
+  let page = $state(0);
+  let items = $state([]);
 
   // Create a collection containing the children of the table
-  const table = new CollectionBuilder($store)
+  const table = new CollectionBuilder(store)
     .setProperty(core.properties.parent)
     .setValue('${this.resource.subject}')
     .setSortBy(commits.properties.createdAt)
     .build();
 
-  $: table.getMembersOnPage(page).then(members => {
-    items = members;
+  $effect(() => {
+    table.getMembersOnPage(page).then(members => {
+      items = members;
+    });
   });
 </script>
 
@@ -511,18 +523,22 @@ ${imports}
     </li>
   {/each}
 </ul>
-<button on:click={() => page -= 1}>Prev</button>
-<button on:click={() => page += 1}>Next</button>`,
+<button onclick={() => page -= 1}>Prev</button>
+<button onclick={() => page += 1}>Next</button>`,
       // Item code
       `// Item.svelte
 <script lang='ts'>
 ${itemImports}
-  export let subject: string;
+  type Props = {
+    subject: string;
+  };
+
+  let { subject }: Props = $props();
 
-  let resource = getResource${genericName}(subject);${hookPart}
+  let resource = getResource${genericName ?? ''}(() => subject);${hookPart}
 </script>
 
-<span>{$resource.title}: {${resourceShorthand ?? '$value'}}</span>`,
+<span>{resource.title}: {${resourceShorthand ?? 'value'}}</span>`,
     ];
   }
 }
diff --git a/browser/data-browser/src/views/OntologyPage/Class/AddPropertyButton.tsx b/browser/data-browser/src/views/OntologyPage/Class/AddPropertyButton.tsx
index b8d3e40e7..c21d50bdc 100644
--- a/browser/data-browser/src/views/OntologyPage/Class/AddPropertyButton.tsx
+++ b/browser/data-browser/src/views/OntologyPage/Class/AddPropertyButton.tsx
@@ -1,4 +1,4 @@
-import { Datatype, Resource, Store, core, useStore } from '@tomic/react';
+import { Resource, core, useStore } from '@tomic/react';
 import { useRef, useState } from 'react';
 import { styled } from 'styled-components';
 import { transition } from '../../../helpers/transition';
@@ -6,6 +6,7 @@ import { FaPlus } from 'react-icons/fa';
 import { SearchBox } from '../../../components/forms/SearchBox';
 import { focusOffsetElement } from '../../../helpers/focusOffsetElement';
 import { useOntologyContext } from '../OntologyContext';
+import { newProperty } from '../ontologyUtils';
 
 interface AddPropertyButtonProps {
   creator: Resource;
@@ -14,25 +15,6 @@ interface AddPropertyButtonProps {
 
 const BUTTON_WIDTH = 'calc(100% - 5.6rem + 4px)'; //Width is 100% - (2 * 1.8rem for button width) + (2rem for gaps) + (4px for borders)
 
-async function newProperty(shortname: string, parent: Resource, store: Store) {
-  const subject = `${parent.subject}/property/${shortname}`;
-
-  const resource = await store.newResource({
-    subject,
-    parent: parent.subject,
-    isA: core.classes.property,
-    propVals: {
-      [core.properties.shortname]: shortname,
-      [core.properties.description]: 'a property',
-      [core.properties.datatype]: Datatype.STRING,
-    },
-  });
-
-  await resource.save();
-
-  return subject;
-}
-
 export function AddPropertyButton({
   creator,
   type,
diff --git a/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx b/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx
index 6b7f6974a..263858ea7 100644
--- a/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx
+++ b/browser/data-browser/src/views/OntologyPage/Class/ClassCardRead.tsx
@@ -7,7 +7,7 @@ import { FaCube } from 'react-icons/fa';
 import { Column, Row } from '../../../components/Row';
 import Markdown from '../../../components/datatypes/Markdown';
 import { AtomicLink } from '../../../components/AtomicLink';
-import { toAnchorId } from '../toAnchorId';
+import { toAnchorId } from '../../../helpers/toAnchorId';
 import { ViewTransitionProps } from '../../../helpers/ViewTransitionProps';
 import {
   RESOURCE_PAGE_TRANSITION_TAG,
diff --git a/browser/data-browser/src/views/OntologyPage/Class/ClassCardWrite.tsx b/browser/data-browser/src/views/OntologyPage/Class/ClassCardWrite.tsx
index b78769bcd..fb9fe7a5b 100644
--- a/browser/data-browser/src/views/OntologyPage/Class/ClassCardWrite.tsx
+++ b/browser/data-browser/src/views/OntologyPage/Class/ClassCardWrite.tsx
@@ -10,7 +10,7 @@ import InputSwitcher from '../../../components/forms/InputSwitcher';
 import ResourceContextMenu, {
   ContextMenuOptions,
 } from '../../../components/ResourceContextMenu';
-import { toAnchorId } from '../toAnchorId';
+import { toAnchorId } from '../../../helpers/toAnchorId';
 import { AddPropertyButton } from './AddPropertyButton';
 import { ErrorChipInput } from '../../../components/forms/ErrorChip';
 import { useOntologyContext } from '../OntologyContext';
@@ -19,7 +19,11 @@ interface ClassCardWriteProps {
   subject: string;
 }
 
-const contextOptions = [ContextMenuOptions.Delete, ContextMenuOptions.History];
+const contextOptions = [
+  ContextMenuOptions.Open,
+  ContextMenuOptions.Delete,
+  ContextMenuOptions.History,
+];
 
 export function ClassCardWrite({ subject }: ClassCardWriteProps): JSX.Element {
   const resource = useResource(subject);
@@ -61,6 +65,7 @@ export function ClassCardWrite({ subject }: ClassCardWriteProps): JSX.Element {
             />
           </TitleWrapper>
           <ResourceContextMenu
+            external
             subject={subject}
             showOnly={contextOptions}
             onAfterDelete={handleDelete}
diff --git a/browser/data-browser/src/views/OntologyPage/CreateInstanceButton.tsx b/browser/data-browser/src/views/OntologyPage/CreateInstanceButton.tsx
index d639a2bd0..fb1b12c9a 100644
--- a/browser/data-browser/src/views/OntologyPage/CreateInstanceButton.tsx
+++ b/browser/data-browser/src/views/OntologyPage/CreateInstanceButton.tsx
@@ -13,10 +13,22 @@ interface CreateInstanceButtonProps {
 export function CreateInstanceButton({ ontology }: CreateInstanceButtonProps) {
   const [classSelectorActive, setClassSelectorActive] = useState(false);
   const [classSubject, setClassSubject] = useState<string | undefined>();
+  const [createdInstanceSubject, setCreatedInstanceSubject] =
+    useState<string>();
+
   const [dialogProps, show, close, isOpen] = useDialog({
-    onSuccess: () => {
-      setClassSubject(undefined);
-      ontology.save();
+    onSuccess: async () => {
+      ontology.push(core.properties.instances, [createdInstanceSubject], true);
+      await ontology.save();
+
+      requestAnimationFrame(() => {
+        document
+          .querySelector(`[about="${createdInstanceSubject}"]`)
+          ?.scrollIntoView({ behavior: 'smooth' });
+
+        setCreatedInstanceSubject(undefined);
+        setClassSubject(undefined);
+      });
     },
   });
 
@@ -31,7 +43,7 @@ export function CreateInstanceButton({ ontology }: CreateInstanceButtonProps) {
   };
 
   const handleSaveClick = (subject: string) => {
-    ontology.push(core.properties.instances, [subject], true);
+    setCreatedInstanceSubject(subject);
     close(true);
   };
 
diff --git a/browser/data-browser/src/views/OntologyPage/DashedButton.tsx b/browser/data-browser/src/views/OntologyPage/DashedButton.tsx
new file mode 100644
index 000000000..f1272af25
--- /dev/null
+++ b/browser/data-browser/src/views/OntologyPage/DashedButton.tsx
@@ -0,0 +1,24 @@
+import { styled } from 'styled-components';
+import { transition } from '../../helpers/transition';
+
+export const DashedButton = styled.button<{ buttonHeight?: string }>`
+  width: 100%;
+  height: ${p => p.buttonHeight ?? '20rem'};
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 1ch;
+  appearance: none;
+  background: none;
+  border: 2px dashed ${p => p.theme.colors.bg2};
+  border-radius: ${p => p.theme.radius};
+  color: ${p => p.theme.colors.textLight};
+  cursor: pointer;
+  ${transition('background', 'color', 'border-color')}
+  &:hover,
+  &:focus-visible {
+    background: ${p => p.theme.colors.bg};
+    border-color: ${p => p.theme.colors.main};
+    color: ${p => p.theme.colors.main};
+  }
+`;
diff --git a/browser/data-browser/src/views/OntologyPage/InfoTitle.tsx b/browser/data-browser/src/views/OntologyPage/InfoTitle.tsx
new file mode 100644
index 000000000..3f1561b2c
--- /dev/null
+++ b/browser/data-browser/src/views/OntologyPage/InfoTitle.tsx
@@ -0,0 +1,41 @@
+import { FaInfo } from 'react-icons/fa6';
+import {
+  IconButton,
+  IconButtonVariant,
+} from '../../components/IconButton/IconButton';
+import { useState } from 'react';
+import { Collapse } from '../../components/Collapse';
+import { Row } from '../../components/Row';
+import Markdown from '../../components/datatypes/Markdown';
+
+interface InfoTitleProps {
+  info: string;
+}
+
+export function InfoTitle({
+  info,
+  children,
+}: React.PropsWithChildren<InfoTitleProps>) {
+  const [collapsed, setCollapsed] = useState(true);
+
+  return (
+    <div>
+      <Row center>
+        <h2>{children}</h2>
+        <IconButton
+          variant={IconButtonVariant.Outline}
+          color='textLight'
+          type='button'
+          size='0.8rem'
+          onClick={() => setCollapsed(prev => !prev)}
+          title='Show helper'
+        >
+          <FaInfo />
+        </IconButton>
+      </Row>
+      <Collapse open={!collapsed}>
+        <Markdown text={info} />
+      </Collapse>
+    </div>
+  );
+}
diff --git a/browser/data-browser/src/views/OntologyPage/InlineDatatype.tsx b/browser/data-browser/src/views/OntologyPage/InlineDatatype.tsx
index 49ecb332d..7c4ea60b8 100644
--- a/browser/data-browser/src/views/OntologyPage/InlineDatatype.tsx
+++ b/browser/data-browser/src/views/OntologyPage/InlineDatatype.tsx
@@ -7,7 +7,7 @@ import {
   useResource,
 } from '@tomic/react';
 import { ResourceInline } from '../ResourceInline';
-import { toAnchorId } from './toAnchorId';
+import { toAnchorId } from '../../helpers/toAnchorId';
 import { useOntologyContext } from './OntologyContext';
 
 interface TypeSuffixProps {
diff --git a/browser/data-browser/src/views/OntologyPage/NewClassButton.tsx b/browser/data-browser/src/views/OntologyPage/NewClassButton.tsx
index 52b16ecc9..9df6e8242 100644
--- a/browser/data-browser/src/views/OntologyPage/NewClassButton.tsx
+++ b/browser/data-browser/src/views/OntologyPage/NewClassButton.tsx
@@ -2,7 +2,6 @@ import { Datatype, Resource, useStore, validateDatatype } from '@tomic/react';
 import { useRef, useState } from 'react';
 import { FaPlus } from 'react-icons/fa';
 import { styled } from 'styled-components';
-import { transition } from '../../helpers/transition';
 import {
   Dialog,
   DialogActions,
@@ -14,8 +13,9 @@ import { Button } from '../../components/Button';
 import { InputStyled, InputWrapper } from '../../components/forms/InputStyles';
 import { stringToSlug } from '../../helpers/stringToSlug';
 import { Column } from '../../components/Row';
-import { newClass, subjectForClass } from './newClass';
-import { toAnchorId } from './toAnchorId';
+import { newClass, subjectForClass } from './ontologyUtils';
+import { toAnchorId } from '../../helpers/toAnchorId';
+import { DashedButton } from './DashedButton';
 
 interface NewClassButtonProps {
   resource: Resource;
@@ -28,6 +28,7 @@ export function NewClassButton({ resource }: NewClassButtonProps): JSX.Element {
   const inputRef = useRef<HTMLInputElement>(null);
 
   const subject = subjectForClass(resource, inputValue);
+  const shortSubject = new URL(subject).pathname;
 
   const [dialogProps, show, hide, isOpen] = useDialog({
     onSuccess: async () => {
@@ -101,7 +102,7 @@ export function NewClassButton({ resource }: NewClassButtonProps): JSX.Element {
                   />
                 </InputWrapper>
 
-                <SubjectWrapper key={0}>{subject}</SubjectWrapper>
+                <SubjectWrapper>{shortSubject}</SubjectWrapper>
               </Column>
             </DialogContent>
             <DialogActions>
@@ -119,28 +120,6 @@ export function NewClassButton({ resource }: NewClassButtonProps): JSX.Element {
   );
 }
 
-const DashedButton = styled.button`
-  width: 100%;
-  height: 20rem;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  gap: 1ch;
-  appearance: none;
-  background: none;
-  border: 2px dashed ${p => p.theme.colors.bg2};
-  border-radius: ${p => p.theme.radius};
-  color: ${p => p.theme.colors.textLight};
-  cursor: pointer;
-  &:hover,
-  &:focus-visible {
-    background: ${p => p.theme.colors.bg};
-    border-color: ${p => p.theme.colors.main};
-    color: ${p => p.theme.colors.main};
-  }
-  ${transition('background', 'color', 'border-color')}
-`;
-
 const SubjectWrapper = styled.div`
   width: 100%;
   max-width: 60ch;
diff --git a/browser/data-browser/src/views/OntologyPage/NewPropertyButton.tsx b/browser/data-browser/src/views/OntologyPage/NewPropertyButton.tsx
new file mode 100644
index 000000000..d81f749bb
--- /dev/null
+++ b/browser/data-browser/src/views/OntologyPage/NewPropertyButton.tsx
@@ -0,0 +1,128 @@
+import {
+  Datatype,
+  Resource,
+  useStore,
+  validateDatatype,
+  type Core,
+} from '@tomic/react';
+import { useRef, useState } from 'react';
+import { FaPlus } from 'react-icons/fa';
+import {
+  Dialog,
+  DialogActions,
+  DialogContent,
+  DialogTitle,
+  useDialog,
+} from '../../components/Dialog';
+import { Button } from '../../components/Button';
+import { InputStyled, InputWrapper } from '../../components/forms/InputStyles';
+import { stringToSlug } from '../../helpers/stringToSlug';
+import { Column } from '../../components/Row';
+import { newProperty } from './ontologyUtils';
+import { toAnchorId } from '../../helpers/toAnchorId';
+import { DashedButton } from './DashedButton';
+import { useOntologyContext } from './OntologyContext';
+
+interface NewPropertyButtonProps {
+  parent: Resource<Core.Ontology>;
+}
+
+export function NewPropertyButton({
+  parent,
+}: NewPropertyButtonProps): JSX.Element {
+  const store = useStore();
+  const [inputValue, setInputValue] = useState('');
+  const [isValid, setIsValid] = useState(false);
+  const inputRef = useRef<HTMLInputElement>(null);
+  const { addProperty } = useOntologyContext();
+
+  const [dialogProps, show, hide, isOpen] = useDialog({
+    onSuccess: async () => {
+      const createdProperty = await newProperty(inputValue, parent, store);
+      await addProperty(createdProperty);
+      requestAnimationFrame(() => {
+        const id = toAnchorId(createdProperty);
+        document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' });
+      });
+    },
+  });
+
+  const handleShortNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    const slugValue = stringToSlug(e.target.value);
+    setInputValue(slugValue);
+    validate(slugValue);
+  };
+
+  const validate = (value: string) => {
+    if (!value) {
+      setIsValid(false);
+
+      return;
+    }
+
+    try {
+      validateDatatype(value, Datatype.SLUG);
+      setIsValid(true);
+    } catch (e) {
+      setIsValid(false);
+    }
+  };
+
+  const openAndReset = () => {
+    setInputValue('');
+    setIsValid(false);
+    show();
+
+    requestAnimationFrame(() => {
+      inputRef.current?.focus();
+    });
+  };
+
+  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
+    if (e.key === 'Escape') {
+      hide(false);
+    }
+
+    if (e.key === 'Enter' && isValid) {
+      hide(true);
+    }
+  };
+
+  return (
+    <>
+      <DashedButton onClick={openAndReset}>
+        <FaPlus /> Add property
+      </DashedButton>
+      <Dialog {...dialogProps}>
+        {isOpen && (
+          <>
+            <DialogTitle>
+              <h1>New Property</h1>
+            </DialogTitle>
+            <DialogContent>
+              <Column>
+                <InputWrapper>
+                  <InputStyled
+                    ref={inputRef}
+                    placeholder='shortname'
+                    value={inputValue}
+                    onChange={handleShortNameChange}
+                    onKeyDown={handleKeyDown}
+                  />
+                </InputWrapper>
+              </Column>
+            </DialogContent>
+            <DialogActions>
+              <Button onClick={() => hide(false)} subtle>
+                Cancel
+              </Button>
+              <Button onClick={() => hide(true)} disabled={!isValid}>
+                Save
+              </Button>
+            </DialogActions>
+          </>
+        )}
+      </Dialog>
+    </>
+  );
+}
diff --git a/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx b/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx
index 9148e5602..ca22bed8a 100644
--- a/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx
+++ b/browser/data-browser/src/views/OntologyPage/OntologyPage.tsx
@@ -1,5 +1,11 @@
 import { ResourcePageProps } from '../ResourcePage';
-import { core, useArray, useCanWrite } from '@tomic/react';
+import {
+  core,
+  useArray,
+  useCanWrite,
+  useResource,
+  type Core,
+} from '@tomic/react';
 import { OntologySidebar } from './OntologySidebar';
 import { styled } from 'styled-components';
 import { ClassCardRead } from './Class/ClassCardRead';
@@ -11,16 +17,29 @@ import { FaEdit, FaEye } from 'react-icons/fa';
 import { OntologyDescription } from './OntologyDescription';
 import { ClassCardWrite } from './Class/ClassCardWrite';
 import { NewClassButton } from './NewClassButton';
-import { toAnchorId } from './toAnchorId';
+import { toAnchorId } from '../../helpers/toAnchorId';
 import { OntologyContextProvider } from './OntologyContext';
 import { PropertyCardWrite } from './Property/PropertyCardWrite';
 import { Graph } from './Graph';
 import { CreateInstanceButton } from './CreateInstanceButton';
 import { useState } from 'react';
+import { NewPropertyButton } from './NewPropertyButton';
+import { InfoTitle } from './InfoTitle';
 
 const isEmpty = (arr: Array<unknown>) => arr.length === 0;
 
 export function OntologyPage({ resource }: ResourcePageProps) {
+  const classesPropResource = useResource<Core.Property>(
+    core.properties.classes,
+  );
+  const propertiesPropResource = useResource<Core.Property>(
+    core.properties.properties,
+  );
+
+  const instancesPropResource = useResource<Core.Property>(
+    core.properties.instances,
+  );
+
   const [classes] = useArray(resource, core.properties.classes);
   const [properties] = useArray(resource, core.properties.properties);
   const [instances] = useArray(resource, core.properties.instances);
@@ -56,7 +75,9 @@ export function OntologyPage({ resource }: ResourcePageProps) {
         <ListSlot>
           <Column style={{ paddingBottom: '3rem' }}>
             <OntologyDescription edit={editMode} resource={resource} />
-            <h2>Classes</h2>
+            <InfoTitle info={classesPropResource.props.description}>
+              Classes
+            </InfoTitle>
             <StyledUl>
               {editMode && (
                 <li>
@@ -72,9 +93,17 @@ export function OntologyPage({ resource }: ResourcePageProps) {
                   )}
                 </li>
               ))}
+              {!editMode && classes.length === 0 && <span>No classes</span>}
             </StyledUl>
-            <h2>Properties</h2>
+            <InfoTitle info={propertiesPropResource.props.description}>
+              Properties
+            </InfoTitle>
             <StyledUl>
+              {editMode && (
+                <li>
+                  <NewPropertyButton parent={resource} />
+                </li>
+              )}
               {properties.map(c => (
                 <li key={c}>
                   {editMode ? (
@@ -84,15 +113,21 @@ export function OntologyPage({ resource }: ResourcePageProps) {
                   )}
                 </li>
               ))}
+              {!editMode && properties.length === 0 && (
+                <span>No properties</span>
+              )}
             </StyledUl>
-            <h2>Instances</h2>
+            <InfoTitle info={instancesPropResource.props.description}>
+              Instances
+            </InfoTitle>
             <StyledUl>
+              {editMode && <CreateInstanceButton ontology={resource} />}
               {instances.map(c => (
                 <li key={c}>
                   <ResourceCard subject={c} id={toAnchorId(c)} />
                 </li>
               ))}
-              {editMode && <CreateInstanceButton ontology={resource} />}
+              {!editMode && instances.length === 0 && <span>No instances</span>}
             </StyledUl>
           </Column>
         </ListSlot>
diff --git a/browser/data-browser/src/views/OntologyPage/OntologySidebar.tsx b/browser/data-browser/src/views/OntologyPage/OntologySidebar.tsx
index 172c7fcd7..a3c900362 100644
--- a/browser/data-browser/src/views/OntologyPage/OntologySidebar.tsx
+++ b/browser/data-browser/src/views/OntologyPage/OntologySidebar.tsx
@@ -4,7 +4,7 @@ import { styled } from 'styled-components';
 import { Details } from '../../components/Details';
 import { FaAtom, FaCube, FaHashtag } from 'react-icons/fa';
 import { ScrollArea } from '../../components/ScrollArea';
-import { toAnchorId } from './toAnchorId';
+import { toAnchorId } from '../../helpers/toAnchorId';
 
 interface OntologySidebarProps {
   ontology: Resource;
diff --git a/browser/data-browser/src/views/OntologyPage/Property/EnumFormPart.tsx b/browser/data-browser/src/views/OntologyPage/Property/EnumFormPart.tsx
index c4c1b1d44..7484a0c79 100644
--- a/browser/data-browser/src/views/OntologyPage/Property/EnumFormPart.tsx
+++ b/browser/data-browser/src/views/OntologyPage/Property/EnumFormPart.tsx
@@ -89,7 +89,7 @@ const TagPanel: FC<TagPanelProps> = ({ resource, ontology }) => {
 
   return (
     <Column>
-      <p>Only allow its value to be one of the following tags:</p>
+      <p>Only allow its value to be selected from the following tags:</p>
       <Row wrapItems>
         {tags.map(tag => (
           <EditableTag subject={tag} key={tag} onDelete={removeTag} />
diff --git a/browser/data-browser/src/views/OntologyPage/Property/PropertyCardRead.tsx b/browser/data-browser/src/views/OntologyPage/Property/PropertyCardRead.tsx
index 7dc55230b..532331a43 100644
--- a/browser/data-browser/src/views/OntologyPage/Property/PropertyCardRead.tsx
+++ b/browser/data-browser/src/views/OntologyPage/Property/PropertyCardRead.tsx
@@ -7,7 +7,7 @@ import { Column, Row } from '../../../components/Row';
 import { InlineFormattedResourceList } from '../../../components/InlineFormattedResourceList';
 import { InlineDatatype } from '../InlineDatatype';
 import { AtomicLink } from '../../../components/AtomicLink';
-import { toAnchorId } from '../toAnchorId';
+import { toAnchorId } from '../../../helpers/toAnchorId';
 
 interface PropertyCardReadProps {
   subject: string;
diff --git a/browser/data-browser/src/views/OntologyPage/Property/PropertyCardWrite.tsx b/browser/data-browser/src/views/OntologyPage/Property/PropertyCardWrite.tsx
index 78b550cbe..e017f6ec7 100644
--- a/browser/data-browser/src/views/OntologyPage/Property/PropertyCardWrite.tsx
+++ b/browser/data-browser/src/views/OntologyPage/Property/PropertyCardWrite.tsx
@@ -4,7 +4,7 @@ import { urls, useCanWrite, useProperty, useResource } from '@tomic/react';
 import { FaHashtag } from 'react-icons/fa';
 import { styled } from 'styled-components';
 import { Column, Row } from '../../../components/Row';
-import { toAnchorId } from '../toAnchorId';
+import { toAnchorId } from '../../../helpers/toAnchorId';
 import InputSwitcher from '../../../components/forms/InputSwitcher';
 import ResourceContextMenu, {
   ContextMenuOptions,
@@ -16,7 +16,11 @@ interface PropertyCardWriteProps {
   subject: string;
 }
 
-const contextOptions = [ContextMenuOptions.Delete, ContextMenuOptions.History];
+const contextOptions = [
+  ContextMenuOptions.Open,
+  ContextMenuOptions.Delete,
+  ContextMenuOptions.History,
+];
 
 export function PropertyCardWrite({
   subject,
@@ -44,6 +48,7 @@ export function PropertyCardWrite({
             />
           </TitleWrapper>
           <ResourceContextMenu
+            external
             subject={subject}
             showOnly={contextOptions}
             onAfterDelete={handleDelete}
diff --git a/browser/data-browser/src/views/OntologyPage/Property/PropertyFormCommon.tsx b/browser/data-browser/src/views/OntologyPage/Property/PropertyFormCommon.tsx
index 9cf980946..b3b48ae80 100644
--- a/browser/data-browser/src/views/OntologyPage/Property/PropertyFormCommon.tsx
+++ b/browser/data-browser/src/views/OntologyPage/Property/PropertyFormCommon.tsx
@@ -13,8 +13,8 @@ import { Column, Row } from '../../../components/Row';
 import { SearchBox } from '../../../components/forms/SearchBox';
 import { OntologyDescription } from '../OntologyDescription';
 import { PropertyDatatypePicker } from '../PropertyDatatypePicker';
-import { newClass } from '../newClass';
-import { toAnchorId } from '../toAnchorId';
+import { newClass } from '../ontologyUtils';
+import { toAnchorId } from '../../../helpers/toAnchorId';
 import { useCurrentSubject } from '../../../helpers/useCurrentSubject';
 import InputResourceArray from '../../../components/forms/InputResourceArray';
 import { EnumFormPart } from './EnumFormPart';
diff --git a/browser/data-browser/src/views/OntologyPage/Property/PropertyLineWrite.tsx b/browser/data-browser/src/views/OntologyPage/Property/PropertyLineWrite.tsx
index 4d07b3fee..a6b9c383c 100644
--- a/browser/data-browser/src/views/OntologyPage/Property/PropertyLineWrite.tsx
+++ b/browser/data-browser/src/views/OntologyPage/Property/PropertyLineWrite.tsx
@@ -25,7 +25,7 @@ export function PropertyLineWrite({
   const resource = useResource(subject);
   const shortnameProp = useProperty(core.properties.shortname);
   const descriptionProp = useProperty(core.properties.description);
-  const [dialogProps, show, hide] = useDialog();
+  const [dialogProps, show, hide, isOpen] = useDialog();
   const [canEdit] = useCanWrite(resource);
 
   const { hasProperty } = useOntologyContext();
@@ -37,7 +37,7 @@ export function PropertyLineWrite({
       <ListItem>
         <Row center justify='space-between'>
           <StyledErrorLook>
-            This property does not exist anymore ({subject})
+            This property does not exist any more ({subject})
           </StyledErrorLook>
           <Button alert onClick={() => onRemove(subject)}>
             Remove
@@ -84,7 +84,12 @@ export function PropertyLineWrite({
           </IconButton>
         </Row>
       </Row>
-      <PropertyWriteDialog resource={resource} {...dialogProps} close={hide} />
+      <PropertyWriteDialog
+        resource={resource}
+        {...dialogProps}
+        close={hide}
+        isOpen={isOpen}
+      />
     </ListItem>
   );
 }
diff --git a/browser/data-browser/src/views/OntologyPage/Property/PropertyWriteDialog.tsx b/browser/data-browser/src/views/OntologyPage/Property/PropertyWriteDialog.tsx
index 0d143c6cf..e062ebce6 100644
--- a/browser/data-browser/src/views/OntologyPage/Property/PropertyWriteDialog.tsx
+++ b/browser/data-browser/src/views/OntologyPage/Property/PropertyWriteDialog.tsx
@@ -13,11 +13,13 @@ import { PropertyFormCommon } from './PropertyFormCommon';
 interface PropertyWriteDialogProps {
   resource: Resource;
   close: () => void;
+  isOpen: boolean;
 }
 
 export function PropertyWriteDialog({
   resource,
   close,
+  isOpen,
   ...dialogProps
 }: PropertyWriteDialogProps & InternalDialogProps): JSX.Element {
   const [canEdit] = useCanWrite(resource);
@@ -25,7 +27,7 @@ export function PropertyWriteDialog({
 
   return (
     <Dialog {...dialogProps} width='min(40rem, 90vw)'>
-      {dialogProps.show && (
+      {isOpen && (
         <>
           <DialogTitle>
             <InputSwitcher
diff --git a/browser/data-browser/src/views/OntologyPage/newClass.ts b/browser/data-browser/src/views/OntologyPage/ontologyUtils.ts
similarity index 60%
rename from browser/data-browser/src/views/OntologyPage/newClass.ts
rename to browser/data-browser/src/views/OntologyPage/ontologyUtils.ts
index dcf388cfe..5328cad8f 100644
--- a/browser/data-browser/src/views/OntologyPage/newClass.ts
+++ b/browser/data-browser/src/views/OntologyPage/ontologyUtils.ts
@@ -1,4 +1,4 @@
-import { Resource, Store, core, type Core } from '@tomic/react';
+import { Datatype, Resource, Store, core, type Core } from '@tomic/react';
 import { sortSubjectList } from './sortSubjectList';
 
 const DEFAULT_DESCRIPTION = 'Change me';
@@ -36,3 +36,26 @@ export async function newClass(
 
   return subject;
 }
+
+export async function newProperty(
+  shortname: string,
+  parent: Resource,
+  store: Store,
+) {
+  const subject = `${parent.subject}/property/${shortname}`;
+
+  const resource = await store.newResource({
+    subject,
+    parent: parent.subject,
+    isA: core.classes.property,
+    propVals: {
+      [core.properties.shortname]: shortname,
+      [core.properties.description]: 'a property',
+      [core.properties.datatype]: Datatype.STRING,
+    },
+  });
+
+  await resource.save();
+
+  return subject;
+}
diff --git a/lib/defaults/ontologies.json b/lib/defaults/ontologies.json
index 3ebab917e..c3c4e931e 100644
--- a/lib/defaults/ontologies.json
+++ b/lib/defaults/ontologies.json
@@ -21,7 +21,7 @@
     "@id": "https://atomicdata.dev/properties/properties",
     "https://atomicdata.dev/properties/classtype": "https://atomicdata.dev/classes/Property",
     "https://atomicdata.dev/properties/datatype": "https://atomicdata.dev/datatypes/resourceArray",
-    "https://atomicdata.dev/properties/description": "A list of properties",
+    "https://atomicdata.dev/properties/description": "A list of properties used by the classes of the ontology.\nA Property is a single field in a Class. It's the thing that a property field in an Atom points to. An example is birthdate. An instance of Property requires various Properties, most notably a datatype (e.g. string or integer), a human readable description (such as the thing you're reading), and a shortname.",
     "https://atomicdata.dev/properties/parent": "https://atomicdata.dev/properties",
     "https://atomicdata.dev/properties/shortname": "properties",
     "https://atomicdata.dev/properties/isA": [
@@ -32,7 +32,7 @@
     "@id": "https://atomicdata.dev/properties/classes",
     "https://atomicdata.dev/properties/classtype": "https://atomicdata.dev/classes/Class",
     "https://atomicdata.dev/properties/datatype": "https://atomicdata.dev/datatypes/resourceArray",
-    "https://atomicdata.dev/properties/description": "A list of classes",
+    "https://atomicdata.dev/properties/description": "A list of classes that make up an ontology.\nA Class describes an abstract concept, such as 'Person' or 'Blogpost'. It describes the data shape of data (which fields are required and recommended) and explains what the concept represents. Resources use the is-a attribute to indicate which classes they are instances of.",
     "https://atomicdata.dev/properties/isA": [
       "https://atomicdata.dev/classes/Property"
     ],
@@ -42,7 +42,7 @@
   {
     "@id": "https://atomicdata.dev/properties/instances",
     "https://atomicdata.dev/properties/datatype": "https://atomicdata.dev/datatypes/resourceArray",
-    "https://atomicdata.dev/properties/description": "A list of class instances",
+    "https://atomicdata.dev/properties/description": "A list of instances that are referenced in the ontology. For example, enum values",
     "https://atomicdata.dev/properties/parent": "https://atomicdata.dev/properties",
     "https://atomicdata.dev/properties/shortname": "instances",
     "https://atomicdata.dev/properties/isA": [


Test patch:
diff --git a/browser/e2e/tests/e2e.spec.ts b/browser/e2e/tests/e2e.spec.ts
index df9646aac..bf6d352bc 100644
--- a/browser/e2e/tests/e2e.spec.ts
+++ b/browser/e2e/tests/e2e.spec.ts
@@ -487,7 +487,7 @@ test.describe('data-browser', async () => {
       'Datatype',
       'boolean',
     );
-    await selectDatatypeOption('boolean - Either `true` or `false`');
+    await selectDatatypeOption('booleanEither `true` or `false`');
 
     await currentDialog(page)
       .getByLabel('Description')
diff --git a/browser/e2e/tests/ontology.spec.ts b/browser/e2e/tests/ontology.spec.ts
index c70e1d841..8ea500fcb 100644
--- a/browser/e2e/tests/ontology.spec.ts
+++ b/browser/e2e/tests/ontology.spec.ts
@@ -17,7 +17,7 @@ test.describe('Ontology', async () => {
     const pickOption = async (query: Locator, keyboardSteps?: number) => {
       await page.waitForTimeout(100);
 
-      // Sometimes when the page moves after the dropdown opens, part of the dropdownfalls outside the viewport.
+      // Sometimes when the page moves after the dropdown opens, part of the dropdown falls outside the viewport.
       // In this case we have to use the keyboard because scrolling doesn't seem to work.
       if (keyboardSteps !== undefined) {
         for (let i = 0; i < keyboardSteps; i++) {
@@ -118,12 +118,11 @@ test.describe('Ontology', async () => {
       .nth(1)
       .click();
 
-    await page.getByPlaceholder('Search for a property').fill('color');
-    await expect(page.getByText('The color of something')).toBeVisible();
+    await expect(
+      page.getByText('A textual description of something'),
+    ).toBeVisible();
 
-    await page.keyboard.press('ArrowDown');
-    await page.keyboard.press('ArrowDown');
-    await page.keyboard.press('Enter');
+    await page.getByText('A textual description of something').click();
 
     await page
       .getByRole('button', { name: 'add required property' })
@@ -167,13 +166,11 @@ test.describe('Ontology', async () => {
     // add name property to arrow-kind
     await arrowKindCard.getByTitle('add required property').click();
 
-    await page.getByPlaceholder('Search for a property').fill('name');
-
     await expect(
-      page.getByText('name - The name of a thing or person'),
+      page.getByText('nameThe name of a thing or person'),
     ).toBeVisible();
 
-    await pickOption(page.getByText('name - The name'), 2);
+    await pickOption(page.getByText('nameThe name'), 1);
 
     // add line-type property to arrow-kind
     await arrowKindCard.getByTitle('add recommended property').click();
@@ -243,6 +240,7 @@ test.describe('Ontology', async () => {
       await currentDialog(page).getByLabel('name').fill(name);
       await currentDialog(page).getByRole('button', { name: 'Save' }).click();
 
+      await expect(page.getByText('Resource loading...')).not.toBeVisible();
       await expect(page.getByRole('heading', { name })).toBeVisible();
     };
 
diff --git a/browser/e2e/tests/test-utils.ts b/browser/e2e/tests/test-utils.ts
index 75b66cc88..f9046e440 100644
--- a/browser/e2e/tests/test-utils.ts
+++ b/browser/e2e/tests/test-utils.ts
@@ -238,7 +238,6 @@ export async function fillSearchBox(
   await selector.getByPlaceholder(placeholder).fill(fillText);
 
   return async (name: string) => {
-    await selector.getByTestId('searchbox-results').getByText(name).hover();
     await selector.getByTestId('searchbox-results').getByText(name).click();
   };
 }