diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,7 @@
+# Changelog for `hgg-frame`
+
+## 0.1.0.0 — 2026-07-18
+
+First public release on Hackage.
+
+- DataFrame abstraction: `class PlotData` and the `df |>> spec` binding.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+BSD 3-Clause License
+
+Copyright (c) 2026, Toshiaki Honda
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+   this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from this
+   software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/hgg-frame.cabal b/hgg-frame.cabal
new file mode 100644
--- /dev/null
+++ b/hgg-frame.cabal
@@ -0,0 +1,56 @@
+cabal-version:      3.0
+name:               hgg-frame
+version:            0.1.0.0
+extra-doc-files:    CHANGELOG.md
+synopsis:           DataFrame abstraction (class PlotData) and the df |>> spec binding for hgg
+description:
+  A dataframe-independent abstraction for writing hgg plots as
+  "dataframe + column names".
+  .
+  * 'PlotData' — a typeclass that bridges any dataframe type to a
+    'Resolver' (column name to ColData). Zero-dependency instances for
+    'Map' and assoc-lists are included, so it works without any dataframe
+    library.
+  * '(|>>)' — binds a dataframe to a spec, producing a 'BoundPlot'.
+    (The Hackage @dataframe@ package already uses @|>@, hence @|>>@.)
+  .
+  Rendering wrappers (saveSVGBound etc.) live in the backend packages
+  (hgg-svg etc.), so this package depends only on hgg-core.
+license:            BSD-3-Clause
+homepage:           https://github.com/frenzieddoll/hgg
+license-file:       LICENSE
+author:             Toshiaki Honda
+maintainer:         frenzieddoll@gmail.com
+copyright:          2026 Aelysce Project (Toshiaki Honda)
+category:           Graphics
+build-type:         Simple
+
+common warnings
+  ghc-options:        -Wall -Wcompat -Widentities -Wincomplete-record-updates
+                      -Wincomplete-uni-patterns -Wpartial-fields
+                      -Wredundant-constraints
+
+library
+  import:           warnings
+  exposed-modules:  Graphics.Hgg.Frame
+  hs-source-dirs:   src
+  build-depends:    base       >= 4.17 && < 5
+                  , vector     >= 0.13 && < 0.14
+                  , text       >= 2.0  && < 2.2
+                  , containers >= 0.6  && < 0.8
+                  , hgg-core ^>= 0.1
+  default-language: Haskell2010
+
+test-suite hgg-frame-tests
+  import:           warnings
+  type:             exitcode-stdio-1.0
+  main-is:          Spec.hs
+  hs-source-dirs:   test
+  build-depends:    base
+                  , vector
+                  , text
+                  , containers
+                  , hgg-core
+                  , hgg-frame
+                  , hspec      >= 2.10 && < 2.12
+  default-language: Haskell2010
diff --git a/src/Graphics/Hgg/Frame.hs b/src/Graphics/Hgg/Frame.hs
new file mode 100644
--- /dev/null
+++ b/src/Graphics/Hgg/Frame.hs
@@ -0,0 +1,145 @@
+-- |
+-- Module      : Graphics.Hgg.Frame
+-- Description : DataFrame 抽象 (class PlotData) ─ 列名で図を書くための df 非依存橋
+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
+-- License     : BSD-3-Clause
+--
+-- ggplot2 のように「データフレーム + 列名」 で図を書くための抽象。 Haskell に
+-- 統一 df ライブラリが無い事情に対応し、 **df 型に依存しない** typeclass
+-- 'PlotData' で「列名 → 実ベクタ」 (= 既存の 'Resolver') を取り出す。
+--
+-- @
+-- import           Graphics.Hgg.Easy   (scatter, layer)
+-- import           Graphics.Hgg.Frame
+-- import qualified Data.Map.Strict as M
+--
+-- df = M.fromList [(\"x\", inline [1,2,3]), (\"y\", inline [4,5,6])]
+-- -- df |>> layer (scatter \"x\" \"y\")   -- バインドは A3 ((|>>)) で
+-- @
+--
+-- 本 module はゼロ依存 instance (assoc-list / 'Data.Map.Map') のみを持つ。
+-- Hackage @dataframe@ 等の外部 df 型の instance は各橋 package が所有する
+-- (orphan 回避、 proposal spec-2 §3.1)。
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeFamilies #-}
+module Graphics.Hgg.Frame
+  ( -- * df 抽象
+    PlotData (..)
+    -- * バインド (df → spec)
+  , BoundPlot (..)
+  , (|>>)
+  , BindableSpec (..)
+  , emptyDfDiagnostics
+  , unBound
+    -- * 補助
+  , colLen
+  ) where
+
+import           Graphics.Hgg.Spec     (ColData (..), Resolver, VisualSpec)
+import           Graphics.Hgg.Validate (PlotDiagnostic (..), validatePlotWith)
+import           Data.Map.Strict       (Map)
+import qualified Data.Map.Strict       as M
+import           Data.Text             (Text)
+import qualified Data.Vector           as V
+
+-- ===========================================================================
+-- class PlotData (リッチ版: toResolver + columnNames + nrows)
+-- ===========================================================================
+
+-- | 任意の df 型を hgg の描画系に橋渡しする typeclass。
+--
+-- リッチ版 (proposal spec-2 §3.1 のユーザ決定): 'toResolver' だけでも描画は
+-- できるが、 'columnNames' で **バインド時の列存在検証** (Phase 14 A5) と将来の
+-- pairs / auto-aes が、 'nrows' で **空 df 検出**が可能になる。
+class PlotData df where
+  -- | 列名 → 'ColData'。 既存の 'Resolver' 型をそのまま再利用する
+  --   (= @saveSVG@ 等が第 2 引数に取るのと同じ「実質 df」)。
+  toResolver  :: df -> Resolver
+
+  -- | 全列名。 列存在チェック・将来の auto-aes / pairs に使う。
+  columnNames :: df -> [Text]
+
+  -- | 行数。 空 df (@nrows == 0@) 検出に使う。 列ごとに長さが違う場合は
+  --   **最長の列の長さ**を返す (= 描画が要求しうる最大 index)。
+  nrows       :: df -> Int
+
+-- | 'ColData' (数値列 / 文字列列) の要素数。
+colLen :: ColData -> Int
+colLen (NumData v) = V.length v
+colLen (TxtData v) = V.length v
+
+-- ===========================================================================
+-- ゼロ依存 instance ─ df ライブラリ無しでも使える最小実装
+-- ===========================================================================
+
+-- | assoc-list (列名と列の対の並び)。 列名重複時は **先勝ち** ('lookup' 準拠)。
+instance PlotData [(Text, ColData)] where
+  toResolver  pairs = \name -> lookup name pairs
+  columnNames       = map fst
+  nrows       pairs = maximum (0 : map (colLen . snd) pairs)
+
+-- | 'Data.Map.Strict.Map' 版。 列名重複は Map が解決済 (一意)。
+instance PlotData (Map Text ColData) where
+  toResolver  m = \name -> M.lookup name m
+  columnNames   = M.keys
+  nrows       m = maximum (0 : map colLen (M.elems m))
+
+-- ===========================================================================
+-- バインド境界 ─ df を spec に結びつけた純値 'BoundPlot'
+-- ===========================================================================
+
+-- | df バインド済の plot。 描画関数 (saveSVGBound 等、 backend package 側) が
+-- 消費する束。
+--
+-- 'bpDiagnostics' は '(|>>)' バインド時の検証結果を **値として**運ぶ
+-- (proposal spec-2 §3.3 / Phase 14 検証案1)。 '(|>>)' 自身は例外を投げない
+-- 純関数なので、 @let p = df |>> spec@ を list に詰める・テストで比較するが
+-- 成り立つ (= 「plot は値」)。 Error severity の報告は描画関数が実行時に行う。
+data BoundPlot = BoundPlot
+  { bpResolver    :: Resolver
+  , bpSpec        :: VisualSpec
+  , bpDiagnostics :: [PlotDiagnostic]
+  }
+
+-- | df を spec にバインド。 **純関数** (例外を投げない)。
+--
+-- 演算子が @|>@ でなく @|>>@ なのは、 Hackage @dataframe@ が @|>@ を public
+-- export しており衝突するため (proposal spec-2 / Phase 14 計画書 §設計判断)。
+-- 'infixl' 1 で @<>@ (infixr 6) より弱く、 @df |>> (layer a <> layer b)@ を
+-- カッコ無しで書ける。
+--
+-- 検証 (A5): バインド時に 'validatePlotWith' で spec の 'ColByName' を df の
+-- 'columnNames' と突合し、 結果を 'bpDiagnostics' に **値として**格納する
+-- (存在しない列 → 'ColumnNotFound' + 編集距離 suggestion、 型不一致 →
+-- 'ColumnTypeMismatch'、 必須 aesthetic 欠落、 空 plot を検出)。 空 df
+-- (@nrows == 0@) は専用 error kind が無いため 'PlotInfo' で残す (lenient)。
+-- 例外は一切投げない。 Error の報告は描画関数 ('saveSVGBound') が実行時に行う。
+-- Phase 24 A6: spec 型で束の型を選ぶ (2D = 'BoundPlot'、 3D = BoundPlot3D)。
+-- 2D の意味論は従来と同一 (instance が旧実装そのもの)。 3D instance は
+-- hgg-3d 側 (型の定義 package = 非 orphan)。
+class BindableSpec spec where
+  type BoundOf spec
+  bindData :: PlotData df => df -> spec -> BoundOf spec
+
+instance BindableSpec VisualSpec where
+  type BoundOf VisualSpec = BoundPlot
+  bindData df spec =
+    BoundPlot r spec (emptyDfDiagnostics df ++ validatePlotWith (columnNames df) r spec)
+   where
+    r = toResolver df
+
+-- | 空 df の共通診断 (2D/3D instance で共有)。
+emptyDfDiagnostics :: PlotData df => df -> [PlotDiagnostic]
+emptyDfDiagnostics df
+  | nrows df == 0 = [PlotInfo "DataFrame が空です (nrows == 0)。 描画は空になります。"]
+  | otherwise     = []
+
+(|>>) :: (PlotData df, BindableSpec spec) => df -> spec -> BoundOf spec
+(|>>) = bindData
+infixl 1 |>>
+
+-- | 検証を逃がす raw 経路。 'BoundPlot' から (Resolver, VisualSpec) を取り出し、
+-- 既存の @saveSVGWith@ / @renderSVGWith@ に直接渡せる。
+unBound :: BoundPlot -> (Resolver, VisualSpec)
+unBound (BoundPlot r s _) = (r, s)
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,80 @@
+-- | hgg-frame テスト。 A1 = class PlotData のゼロ依存 instance 検証。
+{-# LANGUAGE OverloadedStrings #-}
+module Main (main) where
+
+-- 注: renderBound (df |>> spec → SVG) の smoke は hgg-svg の test に置く。
+-- frame:test が svg に依存すると cabal が frame↔svg をパッケージ循環と見なすため。
+import           Graphics.Hgg.Frame
+import           Graphics.Hgg.Spec     (ColData (..), layer, scatter)
+import           Graphics.Hgg.Validate (PlotDiagnostic (..), PlotErrorKind (..),
+                                        Severity (..), diagnosticSeverity)
+import           Data.Map.Strict       (Map)
+import qualified Data.Map.Strict       as M
+import           Data.Text             (Text)
+import qualified Data.Vector           as V
+import           Test.Hspec
+
+-- 長さ違いの列を含むサンプル (g が最長 = 4 行)
+assocDF :: [(Text, ColData)]
+assocDF =
+  [ ("x", NumData (V.fromList [1, 2, 3]))
+  , ("g", TxtData (V.fromList ["a", "b", "c", "d"]))
+  ]
+
+mapDF :: Map Text ColData
+mapDF = M.fromList assocDF
+
+-- scatter 用の x/y を持つ df (bind/render テスト用)。 列順違いで同一データ
+xyAssoc :: [(Text, ColData)]
+xyAssoc =
+  [ ("x", NumData (V.fromList [1, 2, 3]))
+  , ("y", NumData (V.fromList [4, 5, 6]))
+  ]
+
+xyMap :: Map Text ColData
+xyMap = M.fromList xyAssoc
+
+main :: IO ()
+main = hspec $ do
+  describe "PlotData [(Text, ColData)]" $ do
+    it "columnNames は宣言順" $
+      columnNames assocDF `shouldBe` ["x", "g"]
+    it "nrows は最長列の長さ" $
+      nrows assocDF `shouldBe` 4
+    it "toResolver は列を引ける" $
+      toResolver assocDF "x" `shouldBe` Just (NumData (V.fromList [1, 2, 3]))
+    it "toResolver は不在列で Nothing" $
+      toResolver assocDF "zzz" `shouldBe` Nothing
+
+  describe "PlotData (Map Text ColData)" $ do
+    it "columnNames は Map のキー (昇順)" $
+      columnNames mapDF `shouldBe` ["g", "x"]
+    it "nrows は最長列の長さ" $
+      nrows mapDF `shouldBe` 4
+    it "空 df の nrows は 0" $
+      nrows (M.empty :: Map Text ColData) `shouldBe` 0
+
+  describe "(|>>) バインド" $ do
+    it "BoundPlot に resolver/spec を載せ、 診断は空 (A3 時点)" $ do
+      let bp = xyMap |>> layer (scatter "x" "y")
+      bpDiagnostics bp `shouldBe` []
+      fmap colLen (bpResolver bp "x") `shouldBe` Just 3
+    it "純値: unBound で (resolver, spec) を取り出せる" $ do
+      let (r, _) = unBound (xyAssoc |>> layer (scatter "x" "y"))
+      fmap colLen (r "y") `shouldBe` Just 3
+    it "全列存在・数値なら診断ゼロ" $
+      bpDiagnostics (xyMap |>> layer (scatter "x" "y")) `shouldBe` []
+
+  describe "(|>>) 検証 (A5、 案1: 純値・例外なし)" $ do
+    it "存在しない列 → ColumnNotFound Error (例外は投げない)" $ do
+      let diags = bpDiagnostics (xyAssoc |>> layer (scatter "x" "nope"))
+          isNopeNotFound (PlotError (ColumnNotFound n _) _) = n == "nope"
+          isNopeNotFound _                                  = False
+      any isNopeNotFound diags `shouldBe` True
+      any ((== SevError) . diagnosticSeverity) diags `shouldBe` True
+    it "空 df → PlotInfo (lenient、 Error ではない)" $ do
+      let emptyAssoc = [] :: [(Text, ColData)]
+          diags = bpDiagnostics (emptyAssoc |>> layer (scatter "x" "y"))
+          isInfo (PlotInfo _) = True
+          isInfo _            = False
+      any isInfo diags `shouldBe` True
