npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

hazo_notes

v1.1.0

Published

Database-backed notes system with file attachment support for the hazo ecosystem

Downloads

765

Readme

hazo_notes

Database-backed notes system with file attachment support for Next.js applications in the hazo ecosystem.

Features

  • Database-backed persistence - Notes stored in PostgreSQL or SQLite via hazo_connect
  • File attachments - Support for images, PDFs, and documents with embed/attach modes
  • Flexible UI styles - Choose between popover or slide panel presentation
  • Smart save modes - Explicit save/cancel buttons or auto-save on blur
  • INI-based configuration - Simple config file for all settings
  • Full TypeScript support - Complete type definitions included
  • Controlled and uncontrolled modes - Works with parent state or manages its own
  • Paste-to-embed images - Paste images directly into notes
  • User attribution - Automatic user profiles with avatars (optional)
  • File reference syntax - Inline file references with <<embed:XXXX>> and <<attach:XXXX>>

Prerequisites

Before installing hazo_notes, ensure you have:

  • Next.js 14+ with React 18+
  • Tailwind CSS configured
  • hazo_connect installed and configured (for database access)
  • PostgreSQL or SQLite database

Installation

npm install hazo_notes

Peer Dependencies

Install these based on your needs:

# Required for UI components
npm install @radix-ui/react-popover @radix-ui/react-dialog react-icons

# Recommended for full functionality
npm install hazo_connect hazo_auth hazo_logs

Quick Start

1. Add the Component

import { HazoNotesIcon } from 'hazo_notes';

function MyComponent() {
  return (
    <div className="flex items-center gap-2">
      <h2>Customer Information</h2>
      <HazoNotesIcon
        ref_id="customer-info-section"
        label="Customer Information"
      />
    </div>
  );
}

2. Create API Route

Create app/api/hazo_notes/[ref_id]/route.ts:

import { createNotesHandler } from 'hazo_notes/api';
import { getHazoConnectSingleton } from 'hazo_connect/nextjs/setup';
// Import your auth and user lookup functions
import { getSession } from '@/lib/auth'; // Replace with your auth
import { getUserById } from '@/lib/users'; // Replace with your user lookup

export const dynamic = 'force-dynamic';

const { GET, POST } = createNotesHandler({
  getHazoConnect: () => getHazoConnectSingleton(),
  getUserIdFromRequest: async (req) => {
    // IMPORTANT: Replace with your authentication logic
    const session = await getSession(req);
    return session?.user?.id || null;
  },
  getUserProfile: async (userId) => {
    // IMPORTANT: Replace with your user profile lookup
    const user = await getUserById(userId);
    return {
      id: userId,
      name: user?.name || 'Unknown User',
      email: user?.email || '',
      profile_image: user?.avatar,
    };
  },
});

export { GET, POST };

3. Set Up Database

Run the migration:

PostgreSQL:

CREATE TABLE hazo_notes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  ref_id UUID NOT NULL,
  note JSONB NOT NULL DEFAULT '[]'::jsonb,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  changed_at TIMESTAMPTZ,
  note_count INTEGER NOT NULL DEFAULT 0
);

CREATE INDEX idx_hazo_notes_ref_id ON hazo_notes(ref_id);

SQLite:

CREATE TABLE IF NOT EXISTS hazo_notes (
  id TEXT PRIMARY KEY,
  ref_id TEXT NOT NULL,
  note TEXT NOT NULL DEFAULT '[]',
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
  changed_at TEXT,
  note_count INTEGER NOT NULL DEFAULT 0
);

CREATE INDEX IF NOT EXISTS idx_hazo_notes_ref_id ON hazo_notes(ref_id);

4. Configure (Optional)

Copy the config template:

mkdir -p config
cp node_modules/hazo_notes/templates/config/hazo_notes_config.ini config/

Edit config/hazo_notes_config.ini to customize behavior.

For detailed setup instructions, see SETUP_CHECKLIST.md.

Usage Examples

Basic Usage

import { HazoNotesIcon } from 'hazo_notes';

export default function FormPage() {
  return (
    <div className="space-y-4">
      <div className="flex items-center justify-between">
        <label>Annual Income</label>
        <HazoNotesIcon
          ref_id="income-field"
          label="Annual Income"
        />
      </div>
      <input type="number" name="income" />
    </div>
  );
}

Result: Click the notes icon to add contextual notes about this field.

With File Attachments

<HazoNotesIcon
  ref_id="contract-review"
  label="Contract Review"
  enable_files={true}
  max_files_per_note={5}
  allowed_file_types={['pdf', 'docx', 'png', 'jpg']}
  max_file_size_mb={10}
/>

Features:

  • Upload files via file picker or paste images
  • Files referenced in note text with <<embed:0001>> or <<attach:0001>>
  • Images display inline, other files show as download links

Slide Panel Style

<HazoNotesIcon
  ref_id="detailed-notes"
  label="Detailed Notes"
  panel_style="slide_panel"
/>

Result: Notes open in a slide-out panel instead of a popover.

Auto-Save Mode

<HazoNotesIcon
  ref_id="quick-notes"
  label="Quick Notes"
  save_mode="auto"
/>

Result: Notes save automatically when panel closes (no save/cancel buttons).

Custom Styling

<HazoNotesIcon
  ref_id="styled-notes"
  label="Styled Notes"
  background_color="bg-blue-50"
  className="ml-2"
/>

Controlled Mode

'use client';

import { useState } from 'react';
import { HazoNotesIcon } from 'hazo_notes';
import type { NoteEntry } from 'hazo_notes/types';

export default function ControlledExample() {
  const [notes, setNotes] = useState<NoteEntry[]>([]);

  return (
    <HazoNotesIcon
      ref_id="controlled-notes"
      label="Controlled Notes"
      notes={notes}
      on_notes_change={setNotes}
    />
  );
}

Use Case: Sync notes with parent component state or external state management.

Configuration

Configuration options in config/hazo_notes_config.ini:

[ui]
# Background color for notes panel (Tailwind CSS class)
background_color = bg-yellow-100

# Panel presentation style: popover | slide_panel
panel_style = popover

# Save behavior: explicit | auto
save_mode = explicit

[storage]
# File storage mode: jsonb | filesystem
file_storage_mode = jsonb

# Path for filesystem storage (only used when file_storage_mode = filesystem)
file_storage_path = /uploads/notes

[files]
# Maximum file size in MB
max_file_size_mb = 10

# Allowed file types (comma-separated extensions, no dots)
allowed_file_types = pdf,png,jpg,jpeg,gif,doc,docx

# Maximum files per single note entry
max_files_per_note = 5

[logging]
# Log file path (relative to application root)
logfile = logs/hazo_notes.log

Component API

HazoNotesIcon Props

interface HazoNotesIconProps {
  // Required
  ref_id: string;                      // Unique identifier for this notes instance

  // Display
  label?: string;                      // Panel header label
  has_notes?: boolean;                 // Show indicator when notes exist
  note_count?: number;                 // Display count badge

  // Controlled mode
  notes?: NoteEntry[];                 // Controlled notes array
  on_notes_change?: (notes: NoteEntry[]) => void;

  // User context
  current_user?: NoteUserInfo;         // User info (auto-fetched if not provided)

  // Configuration overrides
  panel_style?: 'popover' | 'slide_panel';
  save_mode?: 'explicit' | 'auto';
  background_color?: string;           // Tailwind class

  // File options
  enable_files?: boolean;              // Enable file attachments
  max_files_per_note?: number;
  allowed_file_types?: string[];
  max_file_size_mb?: number;

  // Callbacks
  on_open?: () => void;
  on_close?: () => void;

  // Styling
  disabled?: boolean;
  className?: string;
}

Hooks API

use_notes

Manages notes state and API interactions.

import { use_notes } from 'hazo_notes/hooks';

function MyComponent({ refId }: { refId: string }) {
  const {
    notes,
    note_count,
    loading,
    error,
    add_note,
    refresh,
  } = use_notes(refId);

  const handleAddNote = async () => {
    const success = await add_note('This is my note');
    if (success) {
      console.log('Note added!');
    }
  };

  if (loading) return <div>Loading notes...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      <p>{note_count} notes</p>
      <button onClick={handleAddNote}>Add Note</button>
    </div>
  );
}

use_notes_file_upload

Handles file uploads and validation.

import { use_notes_file_upload } from 'hazo_notes/hooks';

function FileUploadExample() {
  const {
    pending_files,
    upload_file,
    remove_file,
    uploading,
    error,
  } = use_notes_file_upload({
    ref_id: 'my-notes',
    max_file_size_mb: 5,
  });

  const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      const uploaded = await upload_file(file, 'attachment');
      if (uploaded) {
        console.log('File uploaded:', uploaded.filename);
      }
    }
  };

  return (
    <div>
      <input type="file" onChange={handleFileSelect} disabled={uploading} />
      {pending_files.map(f => (
        <div key={f.file_no}>
          {f.filename}
          <button onClick={() => remove_file(f.file_no)}>Remove</button>
        </div>
      ))}
    </div>
  );
}

File Attachments

Notes support inline file references in text:

Embed Mode (Images Inline)

Check out this screenshot:
<<embed:0001>>

Result: Image displays directly in the note.

Attach Mode (Download Link)

Download the full report:
<<attach:0001>>

Result: Shows as a clickable download link with file icon.

Paste to Embed

Users can paste images directly into the note textarea - they're automatically uploaded and referenced with <<embed:XXXX>> syntax.

File Storage Modes

JSONB Mode (Default)

[storage]
file_storage_mode = jsonb
  • Files stored as Base64 in database
  • Simpler setup (no file API needed)
  • Good for small files (< 1MB)
  • Works out of the box

Filesystem Mode

[storage]
file_storage_mode = filesystem
file_storage_path = /uploads/notes
  • Files stored on server filesystem
  • Better for large files
  • Requires file upload API route

Create app/api/hazo_notes/files/upload/route.ts:

import { createFilesHandler } from 'hazo_notes/api';
import { getHazoConnectSingleton } from 'hazo_connect/nextjs/setup';

export const dynamic = 'force-dynamic';

const { POST } = createFilesHandler({
  getHazoConnect: () => getHazoConnectSingleton(),
  getUserIdFromRequest: async (req) => {
    const session = await getSession(req);
    return session?.user?.id || null;
  },
  file_storage_mode: 'filesystem',
  file_storage_path: '/uploads/notes',
  max_file_size_mb: 10,
  allowed_file_types: ['pdf', 'png', 'jpg', 'jpeg', 'gif'],
});

export { POST };

Logger Integration (Optional)

Client-Side

// app/providers.tsx
'use client';

import { LoggerProvider } from 'hazo_notes';
import { createClientLogger } from 'hazo_logs/ui';

const logger = createClientLogger({ packageName: 'my_app' });

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <LoggerProvider logger={logger}>
      {children}
    </LoggerProvider>
  );
}

Server-Side

// lib/logger-setup.ts
import { set_server_logger } from 'hazo_notes/lib';
import { createLogger } from 'hazo_logs';

export function initializeLogger() {
  set_server_logger(createLogger('hazo_notes'));
}

TypeScript Types

All types are exported from hazo_notes/types:

import type {
  NoteEntry,
  NoteFile,
  NoteUserInfo,
  HazoNotesIconProps,
  HazoNotesPanelProps,
} from 'hazo_notes/types';

Database Schema

The hazo_notes table stores all notes:

CREATE TABLE hazo_notes (
  id UUID PRIMARY KEY,
  ref_id UUID NOT NULL,              -- Links to parent entity
  note JSONB NOT NULL DEFAULT '[]',  -- Array of note entries
  created_at TIMESTAMPTZ NOT NULL,
  changed_at TIMESTAMPTZ,
  note_count INTEGER NOT NULL DEFAULT 0
);

Each note entry in the JSONB array:

{
  userid: "user-uuid",
  created_at: "2026-01-07T12:30:00.000Z",
  note_text: "This is the note content",
  note_files: [
    {
      file_no: "0001",
      embed_type: "embed",
      filename: "screenshot.png",
      filedata: "base64_data_or_file_path",
      mime_type: "image/png",
      file_size: 12345
    }
  ]
}

Troubleshooting

Notes icon doesn't open panel

Problem: Icon renders but clicking does nothing.

Solution: Install required UI components:

npm install @radix-ui/react-popover @radix-ui/react-dialog

User shows as "Unknown User"

Problem: Notes display but no user names.

Solution: Implement getUserProfile in your API handler:

getUserProfile: async (userId) => {
  const user = await fetchUserFromDatabase(userId);
  return {
    id: userId,
    name: user.name,
    email: user.email,
    profile_image: user.avatar,
  };
}

Notes don't persist

Problem: Notes disappear after refresh.

Solution:

  1. Verify database table exists (run migration)
  2. Check ref_id is consistent
  3. Verify API route is working: curl http://localhost:3000/api/hazo_notes/test-id

File upload fails

Problem: Can't upload files.

Solution:

  • For JSONB mode: Should work out of the box
  • For filesystem mode: Create the files upload API route (see File Storage Modes above)

Authentication errors

Problem: "Unauthorized" when adding notes.

Solution: Implement getUserIdFromRequest to return authenticated user ID:

getUserIdFromRequest: async (req) => {
  const session = await getSession(req);
  if (!session?.user?.id) return null;
  return session.user.id;
}

Examples

See the test-app/ directory for complete working examples:

  • Basic notes: Simple note creation and display
  • Popover style: Notes in a popover
  • Slide panel style: Notes in a slide-out panel
  • With files: File attachment demonstrations
  • Auto-save: Auto-save mode example
  • Multiple instances: Multiple independent notes on one page
  • Controlled mode: Parent state integration

Run the test app:

npm run dev:test-app
# Open http://localhost:3002

Contributing

See SETUP_CHECKLIST.md for development setup instructions.

License

MIT

Related Packages

Support

  • Issues: https://github.com/pub12/hazo_notes/issues
  • Documentation: https://github.com/pub12/hazo_notes#readme